feat: 新增动画资源与庆祝效果,优化布局与标签页配置
This commit is contained in:
@@ -3,12 +3,7 @@
|
|||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
- **Start development server**: `npm start`
|
|
||||||
- **Run on Android**: `npm run android`
|
|
||||||
- **Run on iOS**: `npm run ios`
|
- **Run on iOS**: `npm run ios`
|
||||||
- **Run on Web**: `npm run web`
|
|
||||||
- **Lint**: `npm run lint`
|
|
||||||
- **Reset project**: `npm run reset-project`
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
- **Framework**: React Native (Expo) with TypeScript using Expo Router for file-based navigation
|
- **Framework**: React Native (Expo) with TypeScript using Expo Router for file-based navigation
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { Tabs, usePathname } from 'expo-router';
|
import { Tabs, usePathname } from 'expo-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text, TouchableOpacity, View } from 'react-native';
|
import { Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
||||||
|
import type { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs';
|
||||||
|
|
||||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
@@ -9,57 +10,51 @@ import { ROUTES } from '@/constants/Routes';
|
|||||||
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
|
||||||
|
// Tab configuration
|
||||||
|
type TabConfig = {
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TAB_CONFIGS: Record<string, TabConfig> = {
|
||||||
|
statistics: { icon: 'chart.pie.fill', title: '健康' },
|
||||||
|
explore: { icon: 'magnifyingglass.circle.fill', title: '发现' },
|
||||||
|
goals: { icon: 'flag.fill', title: '习惯' },
|
||||||
|
personal: { icon: 'person.fill', title: '个人' },
|
||||||
|
};
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
// Helper function to determine if a tab is selected
|
||||||
<Tabs
|
const isTabSelected = (routeName: string): boolean => {
|
||||||
initialRouteName="statistics"
|
const routeMap: Record<string, string> = {
|
||||||
screenOptions={({ route }) => {
|
explore: ROUTES.TAB_EXPLORE,
|
||||||
const routeName = route.name;
|
goals: ROUTES.TAB_GOALS,
|
||||||
const isSelected = (routeName === 'explore' && pathname === ROUTES.TAB_EXPLORE) ||
|
statistics: ROUTES.TAB_STATISTICS,
|
||||||
(routeName === 'coach' && pathname === ROUTES.TAB_COACH) ||
|
};
|
||||||
(routeName === 'goals' && pathname === ROUTES.TAB_GOALS) ||
|
|
||||||
(routeName === 'statistics' && pathname === ROUTES.TAB_STATISTICS) ||
|
|
||||||
pathname.includes(routeName);
|
|
||||||
|
|
||||||
return {
|
return routeMap[routeName] === pathname || pathname.includes(routeName);
|
||||||
headerShown: false,
|
};
|
||||||
tabBarActiveTintColor: colorTokens.tabIconSelected,
|
|
||||||
tabBarButton: (props) => {
|
// Custom tab button component
|
||||||
|
const createTabButton = (routeName: string) => (props: any) => {
|
||||||
const { onPress } = props;
|
const { onPress } = props;
|
||||||
|
const tabConfig = TAB_CONFIGS[routeName];
|
||||||
|
|
||||||
|
if (!tabConfig) return null;
|
||||||
|
|
||||||
|
const isSelected = isTabSelected(routeName);
|
||||||
|
|
||||||
const handlePress = (event: any) => {
|
const handlePress = (event: any) => {
|
||||||
if (process.env.EXPO_OS === 'ios') {
|
if (process.env.EXPO_OS === 'ios') {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
}
|
}
|
||||||
onPress && onPress(event);
|
onPress?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 基于 routeName 设置图标与标题,避免 tabBarIcon 的包装导致文字裁剪
|
|
||||||
const getIconAndTitle = () => {
|
|
||||||
switch (routeName) {
|
|
||||||
case 'explore':
|
|
||||||
return { icon: 'magnifyingglass.circle.fill', title: '发现' } as const;
|
|
||||||
case 'coach':
|
|
||||||
return { icon: 'message.fill', title: 'AI' } as const;
|
|
||||||
case 'goals':
|
|
||||||
return { icon: 'flag.fill', title: '习惯' } as const;
|
|
||||||
case 'statistics':
|
|
||||||
return { icon: 'chart.pie.fill', title: '健康' } as const;
|
|
||||||
case 'personal':
|
|
||||||
return { icon: 'person.fill', title: '个人' } as const;
|
|
||||||
default:
|
|
||||||
return { icon: 'circle', title: '' } as const;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { icon, title } = getIconAndTitle();
|
|
||||||
const activeContentColor = colorTokens.tabIconSelected; // 使用专门为Tab定义的选中颜色
|
|
||||||
const inactiveContentColor = colorTokens.tabIconDefault;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
@@ -80,27 +75,32 @@ export default function TabLayout() {
|
|||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<IconSymbol
|
<IconSymbol
|
||||||
size={22}
|
size={22}
|
||||||
name={icon as any}
|
name={tabConfig.icon as any}
|
||||||
color={isSelected ? activeContentColor : inactiveContentColor}
|
color={isSelected ? colorTokens.tabIconSelected : colorTokens.tabIconDefault}
|
||||||
/>
|
/>
|
||||||
{isSelected && !!title && (
|
{isSelected && (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
color: activeContentColor,
|
color: colorTokens.tabIconSelected,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginLeft: 6,
|
marginLeft: 6,
|
||||||
}}
|
}}
|
||||||
// 选中态下不限制行数,避免大屏布局下被裁剪成省略号
|
|
||||||
numberOfLines={0 as any}
|
numberOfLines={0 as any}
|
||||||
>
|
>
|
||||||
{title}
|
{tabConfig.title}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
|
|
||||||
|
// Common screen options
|
||||||
|
const getScreenOptions = (routeName: string): BottomTabNavigationOptions => ({
|
||||||
|
headerShown: false,
|
||||||
|
tabBarActiveTintColor: colorTokens.tabIconSelected,
|
||||||
|
tabBarButton: createTabButton(routeName),
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: TAB_BAR_BOTTOM_OFFSET,
|
bottom: TAB_BAR_BOTTOM_OFFSET,
|
||||||
@@ -116,9 +116,10 @@ export default function TabLayout() {
|
|||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
marginHorizontal: 20,
|
marginHorizontal: 20,
|
||||||
width: '90%',
|
left: 20,
|
||||||
|
right: 20,
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
} as ViewStyle,
|
||||||
tabBarItemStyle: {
|
tabBarItemStyle: {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
height: TAB_BAR_HEIGHT,
|
height: TAB_BAR_HEIGHT,
|
||||||
@@ -128,151 +129,19 @@ export default function TabLayout() {
|
|||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
},
|
},
|
||||||
tabBarShowLabel: false,
|
tabBarShowLabel: false,
|
||||||
};
|
});
|
||||||
}}>
|
|
||||||
|
|
||||||
<Tabs.Screen
|
|
||||||
name="statistics"
|
|
||||||
options={{
|
|
||||||
title: '统计',
|
|
||||||
tabBarIcon: ({ color }) => {
|
|
||||||
const isStatisticsSelected = pathname === '/statistics';
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<Tabs
|
||||||
<IconSymbol size={22} name="chart.pie.fill" color={color} />
|
initialRouteName="statistics"
|
||||||
{isStatisticsSelected && (
|
screenOptions={({ route }) => getScreenOptions(route.name)}
|
||||||
<Text
|
>
|
||||||
numberOfLines={1}
|
|
||||||
style={{
|
|
||||||
color: color,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '600',
|
|
||||||
marginLeft: 6,
|
|
||||||
textAlign: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}>
|
|
||||||
健康
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="explore"
|
|
||||||
options={{
|
|
||||||
title: '发现',
|
|
||||||
href: null,
|
|
||||||
tabBarIcon: ({ color }) => {
|
|
||||||
const isHomeSelected = pathname === '/' || pathname === '/index';
|
|
||||||
return (
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
||||||
<IconSymbol size={22} name="magnifyingglass.circle.fill" color={color} />
|
|
||||||
{isHomeSelected && (
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
style={{
|
|
||||||
color: color,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '600',
|
|
||||||
marginLeft: 6,
|
|
||||||
textAlign: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}>
|
|
||||||
发现
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="coach"
|
|
||||||
options={{
|
|
||||||
title: 'Seal',
|
|
||||||
tabBarIcon: ({ color }) => {
|
|
||||||
const isCoachSelected = pathname === '/coach';
|
|
||||||
return (
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
||||||
<IconSymbol size={22} name="person.3.fill" color={color} />
|
|
||||||
{isCoachSelected && (
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
style={{
|
|
||||||
color: color,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '600',
|
|
||||||
marginLeft: 6,
|
|
||||||
textAlign: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}>
|
|
||||||
Seal
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tabs.Screen
|
<Tabs.Screen name="statistics" options={{ title: '健康' }} />
|
||||||
name="goals"
|
<Tabs.Screen name="explore" options={{ title: '发现', href: null }} />
|
||||||
options={{
|
<Tabs.Screen name="coach" options={{ title: 'AI', href: null }} />
|
||||||
title: '目标',
|
<Tabs.Screen name="goals" options={{ title: '习惯' }} />
|
||||||
tabBarIcon: ({ color }) => {
|
<Tabs.Screen name="personal" options={{ title: '个人' }} />
|
||||||
const isGoalsSelected = pathname === '/goals';
|
|
||||||
return (
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
||||||
<IconSymbol size={22} name="flag.fill" color={color} />
|
|
||||||
{isGoalsSelected && (
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
style={{
|
|
||||||
color: color,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '600',
|
|
||||||
marginLeft: 6,
|
|
||||||
textAlign: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}>
|
|
||||||
目标
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="personal"
|
|
||||||
options={{
|
|
||||||
title: '个人',
|
|
||||||
tabBarIcon: ({ color }) => {
|
|
||||||
const isPersonalSelected = pathname === '/personal';
|
|
||||||
return (
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
||||||
<IconSymbol size={22} name="person.fill" color={color} />
|
|
||||||
{isPersonalSelected && (
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
style={{
|
|
||||||
color: color,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '600',
|
|
||||||
marginLeft: 6,
|
|
||||||
textAlign: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}>
|
|
||||||
个人
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation';
|
||||||
import GoalTemplateModal from '@/components/GoalTemplateModal';
|
import GoalTemplateModal from '@/components/GoalTemplateModal';
|
||||||
import { GoalsPageGuide } from '@/components/GoalsPageGuide';
|
import { GoalsPageGuide } from '@/components/GoalsPageGuide';
|
||||||
import { GuideTestButton } from '@/components/GuideTestButton';
|
import { GuideTestButton } from '@/components/GuideTestButton';
|
||||||
@@ -20,8 +21,9 @@ import { GoalNotificationHelpers } from '@/utils/notificationHelpers';
|
|||||||
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';
|
||||||
|
import * as Haptics from 'expo-haptics';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Alert, FlatList, Image, RefreshControl, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Alert, FlatList, Image, RefreshControl, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
export default function GoalsScreen() {
|
export default function GoalsScreen() {
|
||||||
@@ -58,6 +60,9 @@ export default function GoalsScreen() {
|
|||||||
const [showGuide, setShowGuide] = useState(false); // 控制引导显示
|
const [showGuide, setShowGuide] = useState(false); // 控制引导显示
|
||||||
const [selectedTemplateData, setSelectedTemplateData] = useState<Partial<CreateGoalRequest> | undefined>();
|
const [selectedTemplateData, setSelectedTemplateData] = useState<Partial<CreateGoalRequest> | undefined>();
|
||||||
|
|
||||||
|
// 庆祝动画引用
|
||||||
|
const celebrationAnimationRef = useRef<CelebrationAnimationRef>(null);
|
||||||
|
|
||||||
// 页面聚焦时重新加载数据
|
// 页面聚焦时重新加载数据
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@@ -288,10 +293,24 @@ export default function GoalsScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理任务完成
|
||||||
|
const handleTaskCompleted = (completedTask: TaskListItem) => {
|
||||||
|
// 触发震动反馈
|
||||||
|
if (process.env.EXPO_OS === 'ios') {
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放庆祝动画
|
||||||
|
celebrationAnimationRef.current?.play();
|
||||||
|
|
||||||
|
console.log(`任务 "${completedTask.title}" 已完成,播放庆祝动画`);
|
||||||
|
};
|
||||||
|
|
||||||
// 渲染任务项
|
// 渲染任务项
|
||||||
const renderTaskItem = ({ item }: { item: TaskListItem }) => (
|
const renderTaskItem = ({ item }: { item: TaskListItem }) => (
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={item}
|
task={item}
|
||||||
|
onTaskCompleted={handleTaskCompleted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -542,6 +561,12 @@ export default function GoalsScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: '测试庆祝动画',
|
||||||
|
onPress: () => {
|
||||||
|
celebrationAnimationRef.current?.play();
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -549,6 +574,9 @@ export default function GoalsScreen() {
|
|||||||
<Text style={styles.testButtonText}>测试通知</Text>
|
<Text style={styles.testButtonText}>测试通知</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 庆祝动画组件 */}
|
||||||
|
<CelebrationAnimation ref={celebrationAnimationRef} />
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { debounce } from 'lodash';
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
|
Image,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -565,10 +566,10 @@ export default function ExploreScreen() {
|
|||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* 背景渐变 */}
|
{/* 背景渐变 */}
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['#F0F9FF', '#E0F2FE']}
|
colors={['#b9e4f5ff', '#b3f6f8ff', '#e6e5e8ff', '#F3F4F6']}
|
||||||
style={styles.gradientBackground}
|
style={styles.gradientBackground}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 0, y: 1 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 装饰性圆圈 */}
|
{/* 装饰性圆圈 */}
|
||||||
@@ -581,6 +582,23 @@ export default function ExploreScreen() {
|
|||||||
contentContainerStyle={{ paddingBottom: bottomPadding }}
|
contentContainerStyle={{ paddingBottom: bottomPadding }}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
|
{/* 顶部信息栏 */}
|
||||||
|
<View style={styles.headerContainer}>
|
||||||
|
<View style={styles.headerContent}>
|
||||||
|
{/* 左边logo */}
|
||||||
|
<Image
|
||||||
|
source={require('@/assets/images/Sealife.jpeg')}
|
||||||
|
style={styles.logoImage}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 右边文字区域 */}
|
||||||
|
<View style={styles.headerTextContainer}>
|
||||||
|
<Text style={styles.headerTitle}>海豹健康</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<WeightHistoryCard />
|
<WeightHistoryCard />
|
||||||
|
|
||||||
{/* 日期选择器 */}
|
{/* 日期选择器 */}
|
||||||
@@ -749,6 +767,28 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
},
|
},
|
||||||
|
headerContainer: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
headerContent: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
logoImage: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
|
headerTextContainer: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 12,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#192126',
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
|
|||||||
1
assets/lottie/Confetti.json
Normal file
1
assets/lottie/Confetti.json
Normal file
File diff suppressed because one or more lines are too long
1
assets/lottie/mood/mood_demo.json
Normal file
1
assets/lottie/mood/mood_demo.json
Normal file
File diff suppressed because one or more lines are too long
62
components/CelebrationAnimation.tsx
Normal file
62
components/CelebrationAnimation.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import LottieView from 'lottie-react-native';
|
||||||
|
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Dimensions,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
export interface CelebrationAnimationRef {
|
||||||
|
play: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CelebrationAnimationProps {
|
||||||
|
visible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CelebrationAnimation = forwardRef<CelebrationAnimationRef, CelebrationAnimationProps>(
|
||||||
|
({ visible = true }, ref) => {
|
||||||
|
const animationRef = useRef<LottieView>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
play: () => {
|
||||||
|
animationRef.current?.play();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container} pointerEvents="none">
|
||||||
|
<LottieView
|
||||||
|
ref={animationRef}
|
||||||
|
autoPlay={false}
|
||||||
|
loop={false}
|
||||||
|
source={require('@/assets/lottie/Confetti.json')}
|
||||||
|
style={styles.animation}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 9999,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
width: Dimensions.get('window').width,
|
||||||
|
height: Dimensions.get('window').height,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
CelebrationAnimation.displayName = 'CelebrationAnimation';
|
||||||
|
|
||||||
|
export default CelebrationAnimation;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { MoodCheckin, getMoodConfig } from '@/services/moodCheckins';
|
import { MoodCheckin, getMoodConfig } from '@/services/moodCheckins';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import React from 'react';
|
import LottieView from 'lottie-react-native';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
interface MoodCardProps {
|
interface MoodCardProps {
|
||||||
@@ -11,10 +12,26 @@ interface MoodCardProps {
|
|||||||
|
|
||||||
export function MoodCard({ moodCheckin, onPress, isLoading = false }: MoodCardProps) {
|
export function MoodCard({ moodCheckin, onPress, isLoading = false }: MoodCardProps) {
|
||||||
const moodConfig = moodCheckin ? getMoodConfig(moodCheckin.moodType) : null;
|
const moodConfig = moodCheckin ? getMoodConfig(moodCheckin.moodType) : null;
|
||||||
|
const animationRef = useRef<LottieView>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (animationRef.current) {
|
||||||
|
animationRef.current.play();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={onPress} style={styles.moodCardContent} disabled={isLoading}>
|
<TouchableOpacity onPress={onPress} style={styles.moodCardContent} disabled={isLoading}>
|
||||||
|
<View style={styles.moodCardHeader}>
|
||||||
<Text style={styles.cardTitle}>心情</Text>
|
<Text style={styles.cardTitle}>心情</Text>
|
||||||
|
<LottieView
|
||||||
|
ref={animationRef}
|
||||||
|
source={require('@/assets/lottie/mood/mood_demo.json')}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
style={styles.lottieAnimation}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<View style={styles.moodPreview}>
|
<View style={styles.moodPreview}>
|
||||||
<Text style={styles.moodLoadingText}>加载中...</Text>
|
<Text style={styles.moodLoadingText}>加载中...</Text>
|
||||||
@@ -40,11 +57,22 @@ const styles = StyleSheet.create({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
moodCardHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
cardTitle: {
|
cardTitle: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#192126',
|
color: '#192126',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
lottieAnimation: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
|
|
||||||
moodPreview: {
|
moodPreview: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import { Alert, Animated, Image, StyleSheet, Text, TouchableOpacity, View } from
|
|||||||
|
|
||||||
interface TaskCardProps {
|
interface TaskCardProps {
|
||||||
task: TaskListItem;
|
task: TaskListItem;
|
||||||
|
onTaskCompleted?: (task: TaskListItem) => void; // 任务完成回调
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TaskCard: React.FC<TaskCardProps> = ({
|
export const TaskCard: React.FC<TaskCardProps> = ({
|
||||||
task,
|
task,
|
||||||
|
onTaskCompleted,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useColorScheme() ?? 'light';
|
const theme = useColorScheme() ?? 'light';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
@@ -99,6 +101,9 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
}
|
}
|
||||||
})).unwrap();
|
})).unwrap();
|
||||||
|
|
||||||
|
// 触发任务完成回调
|
||||||
|
onTaskCompleted?.(task);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Alert.alert('错误', '完成任务失败,请重试');
|
Alert.alert('错误', '完成任务失败,请重试');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { useWaterDataByDate } from '@/hooks/useWaterData';
|
|||||||
import { getQuickWaterAmount } from '@/utils/userPreferences';
|
import { getQuickWaterAmount } from '@/utils/userPreferences';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import LottieView from 'lottie-react-native';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -31,6 +32,8 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
const currentIntake = waterStats?.totalAmount || 0;
|
const currentIntake = waterStats?.totalAmount || 0;
|
||||||
const targetIntake = dailyWaterGoal || 2000;
|
const targetIntake = dailyWaterGoal || 2000;
|
||||||
|
|
||||||
|
const animationRef = useRef<LottieView>(null);
|
||||||
|
|
||||||
// 为每个时间点创建独立的动画值
|
// 为每个时间点创建独立的动画值
|
||||||
const animatedValues = useMemo(() =>
|
const animatedValues = useMemo(() =>
|
||||||
Array.from({ length: 24 }, () => new Animated.Value(0))
|
Array.from({ length: 24 }, () => new Animated.Value(0))
|
||||||
@@ -119,6 +122,8 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
animationRef.current?.play();
|
||||||
|
|
||||||
// 使用用户配置的快速添加饮水量
|
// 使用用户配置的快速添加饮水量
|
||||||
const waterAmount = quickWaterAmount;
|
const waterAmount = quickWaterAmount;
|
||||||
// 如果有选中日期,则为该日期添加记录;否则为今天添加记录
|
// 如果有选中日期,则为该日期添加记录;否则为今天添加记录
|
||||||
@@ -156,6 +161,20 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
onPress={handleCardPress}
|
onPress={handleCardPress}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
|
<LottieView
|
||||||
|
ref={animationRef}
|
||||||
|
autoPlay={false}
|
||||||
|
loop={false}
|
||||||
|
source={require('@/assets/lottie/Confetti.json')}
|
||||||
|
style={{
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
position: 'absolute',
|
||||||
|
left: '15%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
{/* 标题和加号按钮 */}
|
{/* 标题和加号按钮 */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.title}>喝水</Text>
|
<Text style={styles.title}>喝水</Text>
|
||||||
@@ -166,6 +185,7 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
||||||
{/* 柱状图 */}
|
{/* 柱状图 */}
|
||||||
<View style={styles.chartContainer}>
|
<View style={styles.chartContainer}>
|
||||||
<View style={styles.chartWrapper}>
|
<View style={styles.chartWrapper}>
|
||||||
|
|||||||
@@ -122,6 +122,31 @@ PODS:
|
|||||||
- libwebp/sharpyuv (1.5.0)
|
- libwebp/sharpyuv (1.5.0)
|
||||||
- libwebp/webp (1.5.0):
|
- libwebp/webp (1.5.0):
|
||||||
- libwebp/sharpyuv
|
- libwebp/sharpyuv
|
||||||
|
- lottie-ios (4.5.0)
|
||||||
|
- lottie-react-native (7.3.4):
|
||||||
|
- DoubleConversion
|
||||||
|
- glog
|
||||||
|
- lottie-ios (= 4.5.0)
|
||||||
|
- RCT-Folly (= 2024.11.18.00)
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-ImageManager
|
||||||
|
- React-jsc
|
||||||
|
- React-jsi
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-renderercss
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- Yoga
|
||||||
- PurchasesHybridCommon (16.2.2):
|
- PurchasesHybridCommon (16.2.2):
|
||||||
- RevenueCat (= 5.34.0)
|
- RevenueCat (= 5.34.0)
|
||||||
- QCloudCore (6.5.1):
|
- QCloudCore (6.5.1):
|
||||||
@@ -1990,6 +2015,7 @@ DEPENDENCIES:
|
|||||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||||
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
||||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||||
|
- lottie-react-native (from `../node_modules/lottie-react-native`)
|
||||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||||
- RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
- RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||||
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
||||||
@@ -2078,6 +2104,7 @@ SPEC REPOS:
|
|||||||
- libavif
|
- libavif
|
||||||
- libdav1d
|
- libdav1d
|
||||||
- libwebp
|
- libwebp
|
||||||
|
- lottie-ios
|
||||||
- PurchasesHybridCommon
|
- PurchasesHybridCommon
|
||||||
- QCloudCore
|
- QCloudCore
|
||||||
- QCloudCOSXML
|
- QCloudCOSXML
|
||||||
@@ -2151,6 +2178,8 @@ EXTERNAL SOURCES:
|
|||||||
:podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec"
|
:podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec"
|
||||||
glog:
|
glog:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
|
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
|
||||||
|
lottie-react-native:
|
||||||
|
:path: "../node_modules/lottie-react-native"
|
||||||
RCT-Folly:
|
RCT-Folly:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
||||||
RCTDeprecation:
|
RCTDeprecation:
|
||||||
@@ -2346,6 +2375,8 @@ SPEC CHECKSUMS:
|
|||||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
|
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
|
||||||
|
lottie-react-native: 4969af5ac8f2ed2c562d35b6d3d9fe7d34a1add1
|
||||||
PurchasesHybridCommon: 62f852419aae7041792217593998f7ac3f8b567d
|
PurchasesHybridCommon: 62f852419aae7041792217593998f7ac3f8b567d
|
||||||
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
|
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
|
||||||
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
|
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
|
||||||
|
|||||||
@@ -284,6 +284,8 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/LottiePrivacyInfo.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/lottie-react-native/Lottie_React_Native_Privacy.bundle",
|
||||||
);
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
@@ -307,6 +309,8 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/LottiePrivacyInfo.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Lottie_React_Native_Privacy.bundle",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
|
|||||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -41,6 +41,7 @@
|
|||||||
"expo-task-manager": "^13.1.6",
|
"expo-task-manager": "^13.1.6",
|
||||||
"expo-web-browser": "~14.2.0",
|
"expo-web-browser": "~14.2.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"lottie-react-native": "^7.3.4",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
@@ -9703,6 +9704,26 @@
|
|||||||
"loose-envify": "cli.js"
|
"loose-envify": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lottie-react-native": {
|
||||||
|
"version": "7.3.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/lottie-react-native/-/lottie-react-native-7.3.4.tgz",
|
||||||
|
"integrity": "sha512-XUh7eGFb7ID8JRdU6U4N4cYQeYmjtdQRvd8ZXJ6xrdSsn5gZD0c79ITOREPcwJg4YupBFHgyV1GXdAHQP+KYUQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@lottiefiles/dotlottie-react": "^0.13.5",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": ">=0.46",
|
||||||
|
"react-native-windows": ">=0.63.x"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@lottiefiles/dotlottie-react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native-windows": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"expo-task-manager": "^13.1.6",
|
"expo-task-manager": "^13.1.6",
|
||||||
"expo-web-browser": "~14.2.0",
|
"expo-web-browser": "~14.2.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"lottie-react-native": "^7.3.4",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
|
|||||||
Reference in New Issue
Block a user