feat: 更新隐私同意弹窗和应用名称
- 将应用名称修改为“每日普拉提”,提升品牌识别度 - 新增隐私同意弹窗,确保用户在使用应用前同意隐私政策 - 更新 Redux 状态管理,添加隐私同意状态的处理 - 优化用户信息页面,确保体重和身高的格式化显示 - 更新今日训练页面标题为“快速训练”,提升用户体验 - 添加开发工具函数,便于测试隐私同意功能
This commit is contained in:
2
app.json
2
app.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "digital-pilates",
|
"name": "普拉提助手",
|
||||||
"slug": "digital-pilates",
|
"slug": "digital-pilates",
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default function ExploreScreen() {
|
|||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
|
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
|
||||||
const userProfile = useAppSelector((s) => s.user.profile);
|
const userProfile = useAppSelector((s) => s.user.profile);
|
||||||
|
|
||||||
// 使用 dayjs:当月日期与默认选中“今天”
|
// 使用 dayjs:当月日期与默认选中“今天”
|
||||||
const days = getMonthDaysZh();
|
const days = getMonthDaysZh();
|
||||||
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
||||||
@@ -163,16 +164,6 @@ export default function ExploreScreen() {
|
|||||||
})}
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* 打卡入口 */}
|
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 24, marginBottom: 8 }}>
|
|
||||||
<Text style={styles.sectionTitle}>每日报告</Text>
|
|
||||||
<TouchableOpacity onPress={() => router.push('/checkin/calendar')} accessibilityRole="button">
|
|
||||||
<Text style={{ color: '#6B7280', fontWeight: '700' }}>查看打卡日历</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 取消卡片内 loading,保持静默刷新提升体验 */}
|
|
||||||
|
|
||||||
{/* 指标行:左大卡(训练时间),右两小卡(消耗卡路里、步数) */}
|
{/* 指标行:左大卡(训练时间),右两小卡(消耗卡路里、步数) */}
|
||||||
<View style={styles.metricsRow}>
|
<View style={styles.metricsRow}>
|
||||||
<View style={[styles.trainingCard, styles.metricsLeft]}>
|
<View style={[styles.trainingCard, styles.metricsLeft]}>
|
||||||
@@ -230,8 +221,8 @@ export default function ExploreScreen() {
|
|||||||
|
|
||||||
{/* BMI 指数卡片 */}
|
{/* BMI 指数卡片 */}
|
||||||
<BMICard
|
<BMICard
|
||||||
weight={userProfile?.weight}
|
weight={userProfile?.weight ? parseFloat(userProfile.weight) : undefined}
|
||||||
height={userProfile?.height}
|
height={userProfile?.height ? parseFloat(userProfile.height) : undefined}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|||||||
@@ -301,7 +301,10 @@ export default function HomeScreen() {
|
|||||||
>
|
>
|
||||||
<View style={styles.featureIconWrapper}>
|
<View style={styles.featureIconWrapper}>
|
||||||
<View style={styles.featureIconPlaceholder}>
|
<View style={styles.featureIconPlaceholder}>
|
||||||
<ThemedText style={styles.featureIconText}>💪</ThemedText>
|
<Image
|
||||||
|
source={require('@/assets/images/icons/iconPlan.png')}
|
||||||
|
style={styles.featureIconImage}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<ThemedText style={styles.featureTitle}>计划管理</ThemedText>
|
<ThemedText style={styles.featureTitle}>计划管理</ThemedText>
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ export default function PersonalScreen() {
|
|||||||
// 数据格式化函数
|
// 数据格式化函数
|
||||||
const formatHeight = () => {
|
const formatHeight = () => {
|
||||||
if (userProfile.height == null) return '--';
|
if (userProfile.height == null) return '--';
|
||||||
return `${Math.round(userProfile.height)}cm`;
|
return `${parseFloat(userProfile.height).toFixed(1)}cm`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatWeight = () => {
|
const formatWeight = () => {
|
||||||
if (userProfile.weight == null) return '--';
|
if (userProfile.weight == null) return '--';
|
||||||
return `${Math.round(userProfile.weight * 10) / 10}kg`;
|
return `${parseFloat(userProfile.weight).toFixed(1)}kg`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatAge = () => {
|
const formatAge = () => {
|
||||||
|
|||||||
@@ -4,22 +4,60 @@ import { Stack } from 'expo-router';
|
|||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
|
|
||||||
import { useAppDispatch } from '@/hooks/redux';
|
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
|
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
|
||||||
import { store } from '@/store';
|
import { store } from '@/store';
|
||||||
import { rehydrateUser } from '@/store/userSlice';
|
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import RNExitApp from 'react-native-exit-app';
|
||||||
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { privacyAgreed } = useAppSelector((state) => state.user);
|
||||||
|
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
|
||||||
|
const [userDataLoaded, setUserDataLoaded] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch(rehydrateUser());
|
const loadUserData = async () => {
|
||||||
|
await dispatch(rehydrateUser());
|
||||||
|
setUserDataLoaded(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadUserData();
|
||||||
// 冷启动时清空 AI 教练会话缓存
|
// 冷启动时清空 AI 教练会话缓存
|
||||||
clearAiCoachSessionCache();
|
clearAiCoachSessionCache();
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
return <>{children}</>;
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// 当用户数据加载完成后,检查是否需要显示隐私同意弹窗
|
||||||
|
if (userDataLoaded && !privacyAgreed) {
|
||||||
|
setShowPrivacyModal(true);
|
||||||
|
}
|
||||||
|
}, [userDataLoaded, privacyAgreed]);
|
||||||
|
|
||||||
|
const handlePrivacyAgree = () => {
|
||||||
|
dispatch(setPrivacyAgreed());
|
||||||
|
setShowPrivacyModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrivacyDisagree = () => {
|
||||||
|
RNExitApp.exitApp();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<PrivacyConsentModal
|
||||||
|
visible={showPrivacyModal}
|
||||||
|
onAgree={handlePrivacyAgree}
|
||||||
|
onDisagree={handlePrivacyDisagree}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ export default function TodayWorkoutScreen() {
|
|||||||
|
|
||||||
<SafeAreaView style={styles.contentWrapper}>
|
<SafeAreaView style={styles.contentWrapper}>
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
title="今日训练"
|
title="快速训练"
|
||||||
onBack={() => router.back()}
|
onBack={() => router.back()}
|
||||||
withSafeTop={false}
|
withSafeTop={false}
|
||||||
transparent={true}
|
transparent={true}
|
||||||
|
|||||||
BIN
assets/images/icons/iconPlan.png
Normal file
BIN
assets/images/icons/iconPlan.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
191
components/PrivacyConsentModal.tsx
Normal file
191
components/PrivacyConsentModal.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { router } from 'expo-router';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Dimensions,
|
||||||
|
Modal,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
interface PrivacyConsentModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onAgree: () => void;
|
||||||
|
onDisagree: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PrivacyConsentModal({
|
||||||
|
visible,
|
||||||
|
onAgree,
|
||||||
|
onDisagree,
|
||||||
|
}: PrivacyConsentModalProps) {
|
||||||
|
const handleUserAgreementPress = () => {
|
||||||
|
router.push('/legal/user-agreement');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrivacyPolicyPress = () => {
|
||||||
|
router.push('/legal/privacy-policy');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
statusBarTranslucent
|
||||||
|
>
|
||||||
|
<View style={styles.overlay}>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>欢迎来到普拉提助手</Text>
|
||||||
|
|
||||||
|
<View style={styles.contentContainer}>
|
||||||
|
<Text style={styles.description}>
|
||||||
|
点击"同意并继续"代表您已阅读并理解
|
||||||
|
</Text>
|
||||||
|
<View style={styles.linksContainer}>
|
||||||
|
<TouchableOpacity onPress={handleUserAgreementPress}>
|
||||||
|
<Text style={styles.link}>《用户协议》</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.and}> 和 </Text>
|
||||||
|
<TouchableOpacity onPress={handlePrivacyPolicyPress}>
|
||||||
|
<Text style={styles.link}>《隐私政策》</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.description}>
|
||||||
|
我们深知保护用户隐私信息的重要性,请认真进行阅读。
|
||||||
|
</Text>
|
||||||
|
<View style={styles.viewFullContainer}>
|
||||||
|
<Text style={styles.viewFull}>查看完整版 </Text>
|
||||||
|
<TouchableOpacity onPress={handleUserAgreementPress}>
|
||||||
|
<Text style={styles.link}>《用户协议》</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.viewFull}> 和 </Text>
|
||||||
|
<TouchableOpacity onPress={handlePrivacyPolicyPress}>
|
||||||
|
<Text style={styles.link}>《隐私政策》</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.agreeButton} onPress={onAgree}>
|
||||||
|
<Text style={styles.agreeButtonText}>同意并继续</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.disagreeButton} onPress={onDisagree}>
|
||||||
|
<Text style={styles.disagreeButtonText}>不同意并退出</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
overlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: 24,
|
||||||
|
width: width * 0.85,
|
||||||
|
maxWidth: 400,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
marginBottom: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
characterContainer: {
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
iconText: {
|
||||||
|
fontSize: 48,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
balloons: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -5,
|
||||||
|
right: -25,
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
balloon: {
|
||||||
|
width: 12,
|
||||||
|
height: 16,
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#1F2937',
|
||||||
|
marginBottom: 20,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#6B7280',
|
||||||
|
lineHeight: 20,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
linksContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#8B5FE6',
|
||||||
|
textDecorationLine: 'underline',
|
||||||
|
},
|
||||||
|
and: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#6B7280',
|
||||||
|
},
|
||||||
|
viewFullContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
viewFull: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#6B7280',
|
||||||
|
},
|
||||||
|
agreeButton: {
|
||||||
|
backgroundColor: '#8B5FE6',
|
||||||
|
borderRadius: 25,
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 40,
|
||||||
|
width: '100%',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
agreeButtonText: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
disagreeButton: {
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 40,
|
||||||
|
},
|
||||||
|
disagreeButtonText: {
|
||||||
|
color: '#9CA3AF',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1730,6 +1730,8 @@ PODS:
|
|||||||
- Yoga
|
- Yoga
|
||||||
- RNDateTimePicker (8.4.4):
|
- RNDateTimePicker (8.4.4):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- RNExitApp (2.0.0):
|
||||||
|
- React-Core
|
||||||
- RNGestureHandler (2.24.0):
|
- RNGestureHandler (2.24.0):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
@@ -2010,6 +2012,7 @@ DEPENDENCIES:
|
|||||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||||
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
||||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||||
|
- RNExitApp (from `../node_modules/react-native-exit-app`)
|
||||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||||
- RNScreens (from `../node_modules/react-native-screens`)
|
- RNScreens (from `../node_modules/react-native-screens`)
|
||||||
@@ -2221,6 +2224,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/@react-native-masked-view/masked-view"
|
:path: "../node_modules/@react-native-masked-view/masked-view"
|
||||||
RNDateTimePicker:
|
RNDateTimePicker:
|
||||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||||
|
RNExitApp:
|
||||||
|
:path: "../node_modules/react-native-exit-app"
|
||||||
RNGestureHandler:
|
RNGestureHandler:
|
||||||
:path: "../node_modules/react-native-gesture-handler"
|
:path: "../node_modules/react-native-gesture-handler"
|
||||||
RNReanimated:
|
RNReanimated:
|
||||||
@@ -2334,6 +2339,7 @@ SPEC CHECKSUMS:
|
|||||||
RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f
|
RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f
|
||||||
RNCMaskedView: d4644e239e65383f96d2f32c40c297f09705ac96
|
RNCMaskedView: d4644e239e65383f96d2f32c40c297f09705ac96
|
||||||
RNDateTimePicker: 7d93eacf4bdf56350e4b7efd5cfc47639185e10c
|
RNDateTimePicker: 7d93eacf4bdf56350e4b7efd5cfc47639185e10c
|
||||||
|
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
|
||||||
RNGestureHandler: 6e640921d207f070e4bbcf79f4e6d0eabf323389
|
RNGestureHandler: 6e640921d207f070e4bbcf79f4e6d0eabf323389
|
||||||
RNReanimated: 34e90d19560aebd52a2ad583fdc2de2cf7651bbb
|
RNReanimated: 34e90d19560aebd52a2ad583fdc2de2cf7651bbb
|
||||||
RNScreens: 241cfe8fc82737f3e132dd45779f9512928075b8
|
RNScreens: 241cfe8fc82737f3e132dd45779f9512928075b8
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>digital-pilates</string>
|
<string>普拉提助手</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -38,6 +38,7 @@
|
|||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
"react-native-cos-sdk": "^1.2.1",
|
"react-native-cos-sdk": "^1.2.1",
|
||||||
|
"react-native-exit-app": "^2.0.0",
|
||||||
"react-native-gesture-handler": "~2.24.0",
|
"react-native-gesture-handler": "~2.24.0",
|
||||||
"react-native-health": "^1.19.0",
|
"react-native-health": "^1.19.0",
|
||||||
"react-native-image-viewing": "^0.2.2",
|
"react-native-image-viewing": "^0.2.2",
|
||||||
@@ -10881,6 +10882,12 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-exit-app": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-exit-app/-/react-native-exit-app-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-vr9e/8jgPcKCBw6qo0QLxfeMiTwExydghbYDqpLZYAGWR+6cbgnhvOxwdYj/JWR7ZtOALrRA4GMGSvU/ayxM7w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-native-fit-image": {
|
"node_modules/react-native-fit-image": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
"react-native-cos-sdk": "^1.2.1",
|
"react-native-cos-sdk": "^1.2.1",
|
||||||
|
"react-native-exit-app": "^2.0.0",
|
||||||
"react-native-gesture-handler": "~2.24.0",
|
"react-native-gesture-handler": "~2.24.0",
|
||||||
"react-native-health": "^1.19.0",
|
"react-native-health": "^1.19.0",
|
||||||
"react-native-image-viewing": "^0.2.2",
|
"react-native-image-viewing": "^0.2.2",
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const api = {
|
|||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
authToken: '@auth_token',
|
authToken: '@auth_token',
|
||||||
userProfile: '@user_profile',
|
userProfile: '@user_profile',
|
||||||
|
privacyAgreed: '@privacy_agreed',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export async function loadPersistedToken(): Promise<string | null> {
|
export async function loadPersistedToken(): Promise<string | null> {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export type UserProfile = {
|
|||||||
email?: string;
|
email?: string;
|
||||||
gender?: Gender;
|
gender?: Gender;
|
||||||
birthDate?: string;
|
birthDate?: string;
|
||||||
weight?: number;
|
weight?: string;
|
||||||
height?: number;
|
height?: string;
|
||||||
avatar?: string | null;
|
avatar?: string | null;
|
||||||
dailyStepsGoal?: number; // 每日步数目标(用于 Explore 页等)
|
dailyStepsGoal?: number; // 每日步数目标(用于 Explore 页等)
|
||||||
dailyCaloriesGoal?: number; // 每日卡路里消耗目标
|
dailyCaloriesGoal?: number; // 每日卡路里消耗目标
|
||||||
@@ -22,6 +22,7 @@ export type UserState = {
|
|||||||
profile: UserProfile;
|
profile: UserProfile;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
privacyAgreed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_MEMBER_NAME = '普拉提星球学员';
|
export const DEFAULT_MEMBER_NAME = '普拉提星球学员';
|
||||||
@@ -33,6 +34,7 @@ const initialState: UserState = {
|
|||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
privacyAgreed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoginPayload = Record<string, any> & {
|
export type LoginPayload = Record<string, any> & {
|
||||||
@@ -105,22 +107,30 @@ export const login = createAsyncThunk(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const rehydrateUser = createAsyncThunk('user/rehydrate', async () => {
|
export const rehydrateUser = createAsyncThunk('user/rehydrate', async () => {
|
||||||
const [token, profileStr] = await Promise.all([
|
const [token, profileStr, privacyAgreedStr] = await Promise.all([
|
||||||
loadPersistedToken(),
|
loadPersistedToken(),
|
||||||
AsyncStorage.getItem(STORAGE_KEYS.userProfile),
|
AsyncStorage.getItem(STORAGE_KEYS.userProfile),
|
||||||
|
AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed),
|
||||||
]);
|
]);
|
||||||
await setAuthToken(token);
|
await setAuthToken(token);
|
||||||
let profile: UserProfile = {};
|
let profile: UserProfile = {};
|
||||||
if (profileStr) {
|
if (profileStr) {
|
||||||
try { profile = JSON.parse(profileStr) as UserProfile; } catch { profile = {}; }
|
try { profile = JSON.parse(profileStr) as UserProfile; } catch { profile = {}; }
|
||||||
}
|
}
|
||||||
return { token, profile } as { token: string | null; profile: UserProfile };
|
const privacyAgreed = privacyAgreedStr === 'true';
|
||||||
|
return { token, profile, privacyAgreed } as { token: string | null; profile: UserProfile; privacyAgreed: boolean };
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setPrivacyAgreed = createAsyncThunk('user/setPrivacyAgreed', async () => {
|
||||||
|
await AsyncStorage.setItem(STORAGE_KEYS.privacyAgreed, 'true');
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const logout = createAsyncThunk('user/logout', async () => {
|
export const logout = createAsyncThunk('user/logout', async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
AsyncStorage.removeItem(STORAGE_KEYS.authToken),
|
AsyncStorage.removeItem(STORAGE_KEYS.authToken),
|
||||||
AsyncStorage.removeItem(STORAGE_KEYS.userProfile),
|
AsyncStorage.removeItem(STORAGE_KEYS.userProfile),
|
||||||
|
AsyncStorage.removeItem(STORAGE_KEYS.privacyAgreed),
|
||||||
]);
|
]);
|
||||||
await setAuthToken(null);
|
await setAuthToken(null);
|
||||||
return true;
|
return true;
|
||||||
@@ -176,6 +186,7 @@ const userSlice = createSlice({
|
|||||||
.addCase(rehydrateUser.fulfilled, (state, action) => {
|
.addCase(rehydrateUser.fulfilled, (state, action) => {
|
||||||
state.token = action.payload.token;
|
state.token = action.payload.token;
|
||||||
state.profile = action.payload.profile;
|
state.profile = action.payload.profile;
|
||||||
|
state.privacyAgreed = action.payload.privacyAgreed;
|
||||||
if (!state.profile?.name || !state.profile.name.trim()) {
|
if (!state.profile?.name || !state.profile.name.trim()) {
|
||||||
state.profile.name = DEFAULT_MEMBER_NAME;
|
state.profile.name = DEFAULT_MEMBER_NAME;
|
||||||
}
|
}
|
||||||
@@ -193,6 +204,10 @@ const userSlice = createSlice({
|
|||||||
.addCase(logout.fulfilled, (state) => {
|
.addCase(logout.fulfilled, (state) => {
|
||||||
state.token = null;
|
state.token = null;
|
||||||
state.profile = {};
|
state.profile = {};
|
||||||
|
state.privacyAgreed = false;
|
||||||
|
})
|
||||||
|
.addCase(setPrivacyAgreed.fulfilled, (state) => {
|
||||||
|
state.privacyAgreed = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
47
utils/devTools.ts
Normal file
47
utils/devTools.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { STORAGE_KEYS } from '@/services/api';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发工具函数 - 清除隐私同意状态
|
||||||
|
* 用于测试隐私同意弹窗功能
|
||||||
|
*/
|
||||||
|
export const clearPrivacyAgreement = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.removeItem(STORAGE_KEYS.privacyAgreed);
|
||||||
|
console.log('隐私同意状态已清除');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除隐私同意状态失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发工具函数 - 检查隐私同意状态
|
||||||
|
*/
|
||||||
|
export const checkPrivacyAgreement = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const privacyAgreed = await AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed);
|
||||||
|
const isAgreed = privacyAgreed === 'true';
|
||||||
|
console.log('隐私同意状态:', isAgreed);
|
||||||
|
return isAgreed;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查隐私同意状态失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发工具函数 - 清除所有用户数据
|
||||||
|
* 用于完全重置应用状态
|
||||||
|
*/
|
||||||
|
export const clearAllUserData = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
AsyncStorage.removeItem(STORAGE_KEYS.authToken),
|
||||||
|
AsyncStorage.removeItem(STORAGE_KEYS.userProfile),
|
||||||
|
AsyncStorage.removeItem(STORAGE_KEYS.privacyAgreed),
|
||||||
|
]);
|
||||||
|
console.log('所有用户数据已清除');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除用户数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user