feat: 更新 AI 教练聊天界面和个人信息页面
- 在 AI 教练聊天界面中添加训练记录分析功能,允许用户基于近期训练记录获取分析建议 - 更新 Redux 状态管理,集成每日步数和卡路里目标 - 在个人信息页面中优化用户头像显示,支持从库中选择头像 - 修改首页布局,添加可拖动的教练徽章,提升用户交互体验 - 更新样式以适应新功能的展示和交互
This commit is contained in:
@@ -3,6 +3,7 @@ import { CircularRing } from '@/components/CircularRing';
|
|||||||
import { ProgressBar } from '@/components/ProgressBar';
|
import { ProgressBar } from '@/components/ProgressBar';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||||
|
import { useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
||||||
import { ensureHealthPermissions, fetchHealthDataForDate, fetchTodayHealthData } from '@/utils/health';
|
import { ensureHealthPermissions, fetchHealthDataForDate, fetchTodayHealthData } from '@/utils/health';
|
||||||
@@ -23,6 +24,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
export default function ExploreScreen() {
|
export default function ExploreScreen() {
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
|
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
|
||||||
// 使用 dayjs:当月日期与默认选中“今天”
|
// 使用 dayjs:当月日期与默认选中“今天”
|
||||||
const days = getMonthDaysZh();
|
const days = getMonthDaysZh();
|
||||||
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
||||||
@@ -185,13 +187,13 @@ export default function ExploreScreen() {
|
|||||||
value={stepCount}
|
value={stepCount}
|
||||||
resetToken={animToken}
|
resetToken={animToken}
|
||||||
style={styles.stepsValue}
|
style={styles.stepsValue}
|
||||||
format={(v) => `${Math.round(v)}/2000`}
|
format={(v) => `${Math.round(v)}/${stepGoal}`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.stepsValue}>——/2000</Text>
|
<Text style={styles.stepsValue}>——/{stepGoal}</Text>
|
||||||
)}
|
)}
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
progress={Math.min(1, Math.max(0, (stepCount ?? 0) / 2000))}
|
progress={Math.min(1, Math.max(0, (stepCount ?? 0) / stepGoal))}
|
||||||
height={18}
|
height={18}
|
||||||
trackColor="#FFEBCB"
|
trackColor="#FFEBCB"
|
||||||
fillColor="#FFC365"
|
fillColor="#FFC365"
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { useAuthGuard } from '@/hooks/useAuthGuard';
|
|||||||
import { getChineseGreeting } from '@/utils/date';
|
import { getChineseGreeting } from '@/utils/date';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Pressable, SafeAreaView, ScrollView, StyleSheet, View } from 'react-native';
|
import { Animated, Image, PanResponder, Pressable, SafeAreaView, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
// 移除旧的“热门活动”滑动数据,改为固定的“热点功能”卡片
|
// 移除旧的“热门活动”滑动数据,改为固定的“热点功能”卡片
|
||||||
|
|
||||||
@@ -18,9 +19,108 @@ export default function HomeScreen() {
|
|||||||
const { pushIfAuthedElseLogin } = useAuthGuard();
|
const { pushIfAuthedElseLogin } = useAuthGuard();
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||||
|
|
||||||
|
// Draggable coach badge state
|
||||||
|
const pan = React.useRef(new Animated.ValueXY()).current;
|
||||||
|
const [coachSize, setCoachSize] = React.useState({ width: 0, height: 0 });
|
||||||
|
const hasInitPos = React.useRef(false);
|
||||||
|
const startRef = React.useRef({ x: 0, y: 0 });
|
||||||
|
const dragState = React.useRef({ moved: false });
|
||||||
|
|
||||||
|
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
|
||||||
|
|
||||||
|
const panResponder = React.useMemo(() => PanResponder.create({
|
||||||
|
onStartShouldSetPanResponder: () => true,
|
||||||
|
onMoveShouldSetPanResponder: (_evt, gesture) => Math.abs(gesture.dx) + Math.abs(gesture.dy) > 2,
|
||||||
|
onPanResponderGrant: () => {
|
||||||
|
dragState.current.moved = false;
|
||||||
|
// @ts-ignore access current value
|
||||||
|
const currentX = (pan.x as any)._value ?? 0;
|
||||||
|
// @ts-ignore access current value
|
||||||
|
const currentY = (pan.y as any)._value ?? 0;
|
||||||
|
startRef.current = { x: currentX, y: currentY };
|
||||||
|
},
|
||||||
|
onPanResponderMove: (_evt, gesture) => {
|
||||||
|
if (!dragState.current.moved && (Math.abs(gesture.dx) + Math.abs(gesture.dy) > 4)) {
|
||||||
|
dragState.current.moved = true;
|
||||||
|
}
|
||||||
|
const nextX = startRef.current.x + gesture.dx;
|
||||||
|
const nextY = startRef.current.y + gesture.dy;
|
||||||
|
pan.setValue({ x: nextX, y: nextY });
|
||||||
|
},
|
||||||
|
onPanResponderRelease: (_evt, gesture) => {
|
||||||
|
const minX = 8;
|
||||||
|
const minY = insets.top + 2;
|
||||||
|
const maxX = Math.max(minX, windowWidth - coachSize.width - 8);
|
||||||
|
const maxY = Math.max(minY, windowHeight - coachSize.height - (insets.bottom + 8));
|
||||||
|
const rawX = startRef.current.x + gesture.dx;
|
||||||
|
const rawY = startRef.current.y + gesture.dy;
|
||||||
|
const clampedX = clamp(rawX, minX, maxX);
|
||||||
|
const clampedY = clamp(rawY, minY, maxY);
|
||||||
|
// Snap horizontally to nearest side (left/right only)
|
||||||
|
const distLeft = Math.abs(clampedX - minX);
|
||||||
|
const distRight = Math.abs(maxX - clampedX);
|
||||||
|
const snapX = distLeft <= distRight ? minX : maxX;
|
||||||
|
Animated.spring(pan, { toValue: { x: snapX, y: clampedY }, useNativeDriver: false, bounciness: 6 }).start(() => {
|
||||||
|
if (!dragState.current.moved) {
|
||||||
|
// Treat as tap
|
||||||
|
// @ts-ignore - expo-router string ok
|
||||||
|
router.push('/ai-coach-chat?name=Iris' as any);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}), [coachSize.height, coachSize.width, insets.bottom, insets.top, pan, windowHeight, windowWidth, router]);
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
||||||
<ThemedView style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
<ThemedView style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
||||||
|
{/* Floating Coach Badge */}
|
||||||
|
<View pointerEvents="box-none" style={styles.coachOverlayWrap}>
|
||||||
|
<Animated.View
|
||||||
|
{...panResponder.panHandlers}
|
||||||
|
onLayout={(e) => {
|
||||||
|
const { width, height } = e.nativeEvent.layout;
|
||||||
|
if (width !== coachSize.width || height !== coachSize.height) {
|
||||||
|
setCoachSize({ width, height });
|
||||||
|
}
|
||||||
|
if (!hasInitPos.current && width > 0 && windowWidth > 0) {
|
||||||
|
const initX = windowWidth - width - 14;
|
||||||
|
const initY = insets.top + 2; // 默认更靠上,避免遮挡搜索框
|
||||||
|
pan.setValue({ x: initX, y: initY });
|
||||||
|
hasInitPos.current = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={[
|
||||||
|
styles.coachBadge,
|
||||||
|
{
|
||||||
|
transform: [{ translateX: pan.x }, { translateY: pan.y }],
|
||||||
|
backgroundColor: colorTokens.heroSurfaceTint,
|
||||||
|
borderColor: 'rgba(187,242,70,0.35)',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOpacity: 0.08,
|
||||||
|
shadowRadius: 10,
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
elevation: 3,
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/imageCoach01.jpeg' }}
|
||||||
|
style={styles.coachAvatar}
|
||||||
|
/>
|
||||||
|
<View style={styles.coachMeta}>
|
||||||
|
<ThemedText style={styles.coachName}>Iris</ThemedText>
|
||||||
|
<View style={styles.coachStatusRow}>
|
||||||
|
<View style={styles.statusDot} />
|
||||||
|
<ThemedText style={styles.coachStatusText}>在线</ThemedText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
<ScrollView showsVerticalScrollIndicator={false}>
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@@ -66,7 +166,7 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
<View style={styles.planList}>
|
<View style={styles.planList}>
|
||||||
<PlanCard
|
<PlanCard
|
||||||
image={'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/Imagettpg.png'}
|
image={'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg'}
|
||||||
title="体态评估"
|
title="体态评估"
|
||||||
subtitle="评估你的体态,制定训练计划"
|
subtitle="评估你的体态,制定训练计划"
|
||||||
level="初学者"
|
level="初学者"
|
||||||
@@ -77,7 +177,6 @@ export default function HomeScreen() {
|
|||||||
image={'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/Image30play@2x.png'}
|
image={'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/Image30play@2x.png'}
|
||||||
title="每周打卡"
|
title="每周打卡"
|
||||||
subtitle="养成训练习惯,练出好身材"
|
subtitle="养成训练习惯,练出好身材"
|
||||||
level="初学者"
|
|
||||||
progress={0.75}
|
progress={0.75}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -92,7 +191,7 @@ export default function HomeScreen() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable onPress={() => pushIfAuthedElseLogin('/checkin')}>
|
<Pressable onPress={() => pushIfAuthedElseLogin('/checkin')}>
|
||||||
<PlanCard
|
<PlanCard
|
||||||
image={'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/Image30play@2x.png'}
|
image={'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/ImageCheck.jpeg'}
|
||||||
title="每日打卡(自选动作)"
|
title="每日打卡(自选动作)"
|
||||||
subtitle="选择动作,设置组数/次数,记录完成"
|
subtitle="选择动作,设置组数/次数,记录完成"
|
||||||
level="初学者"
|
level="初学者"
|
||||||
@@ -126,6 +225,14 @@ const styles = StyleSheet.create({
|
|||||||
paddingTop: 16,
|
paddingTop: 16,
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
},
|
},
|
||||||
|
coachOverlayWrap: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
},
|
||||||
greeting: {
|
greeting: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#8A8A8E',
|
color: '#8A8A8E',
|
||||||
@@ -138,6 +245,45 @@ const styles = StyleSheet.create({
|
|||||||
color: '#1A1A1A',
|
color: '#1A1A1A',
|
||||||
lineHeight: 36,
|
lineHeight: 36,
|
||||||
},
|
},
|
||||||
|
coachBadge: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
// RN 不完全支持 gap,这里用 margin 实现
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 20,
|
||||||
|
borderWidth: 1,
|
||||||
|
backgroundColor: '#FFFFFF00',
|
||||||
|
},
|
||||||
|
coachAvatar: {
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 13,
|
||||||
|
},
|
||||||
|
coachMeta: {
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
coachName: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#192126',
|
||||||
|
},
|
||||||
|
coachStatusRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
statusDot: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
borderRadius: 3,
|
||||||
|
backgroundColor: '#22C55E',
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
coachStatusText: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: '#6B7280',
|
||||||
|
},
|
||||||
sectionContainer: {
|
sectionContainer: {
|
||||||
marginTop: 24,
|
marginTop: 24,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||||
import { useAppSelector } from '@/hooks/redux';
|
import { useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import { DEFAULT_MEMBER_NAME } from '@/store/userSlice';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
@@ -9,7 +10,7 @@ import { useFocusEffect } from '@react-navigation/native';
|
|||||||
import type { Href } from 'expo-router';
|
import type { Href } from 'expo-router';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { Alert, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
import { Alert, Image, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function PersonalScreen() {
|
export default function PersonalScreen() {
|
||||||
@@ -24,6 +25,7 @@ export default function PersonalScreen() {
|
|||||||
const colors = Colors[colorScheme ?? 'light'];
|
const colors = Colors[colorScheme ?? 'light'];
|
||||||
const theme = (colorScheme ?? 'light') as 'light' | 'dark';
|
const theme = (colorScheme ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
|
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg';
|
||||||
|
|
||||||
type UserProfile = {
|
type UserProfile = {
|
||||||
fullName?: string;
|
fullName?: string;
|
||||||
@@ -117,25 +119,21 @@ export default function PersonalScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const displayName = (profile.fullName && profile.fullName.trim()) ? profile.fullName : DEFAULT_MEMBER_NAME;
|
||||||
|
|
||||||
const UserInfoSection = () => (
|
const UserInfoSection = () => (
|
||||||
<View style={styles.userInfoCard}>
|
<View style={[styles.userInfoCard, { backgroundColor: colorTokens.card }]}>
|
||||||
<View style={styles.userInfoContainer}>
|
<View style={styles.userInfoContainer}>
|
||||||
{/* 头像 */}
|
{/* 头像 */}
|
||||||
<View style={styles.avatarContainer}>
|
<View style={styles.avatarContainer}>
|
||||||
<View style={styles.avatar}>
|
<View style={[styles.avatar, { backgroundColor: colorTokens.ornamentAccent }]}>
|
||||||
<View style={styles.avatarContent}>
|
<Image source={{ uri: profile.avatarUri || DEFAULT_AVATAR_URL }} style={{ width: '100%', height: '100%' }} />
|
||||||
{/* 简单的头像图标,您可以替换为实际图片 */}
|
|
||||||
<View style={styles.avatarIcon}>
|
|
||||||
<View style={styles.avatarFace} />
|
|
||||||
<View style={styles.avatarBody} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 用户信息 */}
|
{/* 用户信息 */}
|
||||||
<View style={styles.userDetails}>
|
<View style={styles.userDetails}>
|
||||||
<Text style={styles.userName}>{profile.fullName || '未设置姓名'}</Text>
|
<Text style={[styles.userName, { color: colorTokens.text }]}>{displayName}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 编辑按钮 */}
|
{/* 编辑按钮 */}
|
||||||
@@ -147,25 +145,25 @@ export default function PersonalScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const StatsSection = () => (
|
const StatsSection = () => (
|
||||||
<View style={styles.statsContainer}>
|
<View style={[styles.statsContainer, { backgroundColor: colorTokens.card }]}>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Text style={dynamicStyles.statValue}>{formatHeight()}</Text>
|
<Text style={dynamicStyles.statValue}>{formatHeight()}</Text>
|
||||||
<Text style={styles.statLabel}>身高</Text>
|
<Text style={[styles.statLabel, { color: colorTokens.textMuted }]}>身高</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Text style={dynamicStyles.statValue}>{formatWeight()}</Text>
|
<Text style={dynamicStyles.statValue}>{formatWeight()}</Text>
|
||||||
<Text style={styles.statLabel}>体重</Text>
|
<Text style={[styles.statLabel, { color: colorTokens.textMuted }]}>体重</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Text style={dynamicStyles.statValue}>{formatAge()}</Text>
|
<Text style={dynamicStyles.statValue}>{formatAge()}</Text>
|
||||||
<Text style={styles.statLabel}>年龄</Text>
|
<Text style={[styles.statLabel, { color: colorTokens.textMuted }]}>年龄</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
const MenuSection = ({ title, items }: { title: string; items: any[] }) => (
|
const MenuSection = ({ title, items }: { title: string; items: any[] }) => (
|
||||||
<View style={styles.menuSection}>
|
<View style={[styles.menuSection, { backgroundColor: colorTokens.card }]}>
|
||||||
<Text style={styles.sectionTitle}>{title}</Text>
|
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}>{title}</Text>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={index}
|
key={index}
|
||||||
@@ -173,10 +171,10 @@ export default function PersonalScreen() {
|
|||||||
onPress={item.onPress}
|
onPress={item.onPress}
|
||||||
>
|
>
|
||||||
<View style={styles.menuItemLeft}>
|
<View style={styles.menuItemLeft}>
|
||||||
<View style={[styles.menuIcon]}>
|
<View style={[styles.menuIcon, { backgroundColor: 'rgba(187,242,70,0.12)' }]}>
|
||||||
<Ionicons name={item.icon} size={20} color={item.iconColor || colors.primary} />
|
<Ionicons name={item.icon} size={20} color={'#192126'} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.menuItemText}>{item.title}</Text>
|
<Text style={[styles.menuItemText, { color: colorTokens.text }]}>{item.title}</Text>
|
||||||
</View>
|
</View>
|
||||||
{item.type === 'switch' ? (
|
{item.type === 'switch' ? (
|
||||||
<Switch
|
<Switch
|
||||||
@@ -187,7 +185,7 @@ export default function PersonalScreen() {
|
|||||||
style={styles.switch}
|
style={styles.switch}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Ionicons name="chevron-forward" size={20} color="#C4C4C4" />
|
<Ionicons name="chevron-forward" size={20} color={colorTokens.icon} />
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
@@ -290,7 +288,7 @@ export default function PersonalScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
<View style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
|
<StatusBar barStyle={theme === 'light' ? 'dark-content' : 'light-content'} backgroundColor="transparent" translucent />
|
||||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={[styles.scrollView, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}
|
style={[styles.scrollView, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}
|
||||||
@@ -333,6 +331,15 @@ const styles = StyleSheet.create({
|
|||||||
userInfoCard: {
|
userInfoCard: {
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 2,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.08,
|
||||||
|
shadowRadius: 6,
|
||||||
|
elevation: 3,
|
||||||
},
|
},
|
||||||
userInfoContainer: {
|
userInfoContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -380,7 +387,7 @@ const styles = StyleSheet.create({
|
|||||||
userName: {
|
userName: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#000',
|
color: '#192126',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -408,18 +415,19 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
statLabel: {
|
statLabel: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#888',
|
color: '#687076',
|
||||||
},
|
},
|
||||||
// 菜单区域
|
// 菜单区域
|
||||||
menuSection: {
|
menuSection: {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
|
borderRadius: 16,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 18,
|
fontSize: 20,
|
||||||
fontWeight: 'bold',
|
fontWeight: '800',
|
||||||
color: '#000',
|
color: '#192126',
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
},
|
},
|
||||||
@@ -447,7 +455,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
menuItemText: {
|
menuItemText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#000',
|
color: '#192126',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
switch: {
|
switch: {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import React, { useMemo, useRef, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
FlatList,
|
FlatList,
|
||||||
|
Image,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
Platform,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -20,6 +21,7 @@ import { HeaderBar } from '@/components/ui/HeaderBar';
|
|||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppSelector } from '@/hooks/redux';
|
import { useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import type { CheckinRecord } from '@/store/checkinSlice';
|
||||||
|
|
||||||
type Role = 'user' | 'assistant';
|
type Role = 'user' | 'assistant';
|
||||||
|
|
||||||
@@ -29,6 +31,8 @@ type ChatMessage = {
|
|||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const COACH_AVATAR = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/imageCoach01.jpeg';
|
||||||
|
|
||||||
export default function AICoachChatScreen() {
|
export default function AICoachChatScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useLocalSearchParams<{ name?: string }>();
|
const params = useLocalSearchParams<{ name?: string }>();
|
||||||
@@ -47,11 +51,13 @@ export default function AICoachChatScreen() {
|
|||||||
const listRef = useRef<FlatList<ChatMessage>>(null);
|
const listRef = useRef<FlatList<ChatMessage>>(null);
|
||||||
|
|
||||||
const planDraft = useAppSelector((s) => s.trainingPlan?.draft);
|
const planDraft = useAppSelector((s) => s.trainingPlan?.draft);
|
||||||
|
const checkin = useAppSelector((s) => (s as any).checkin);
|
||||||
|
|
||||||
const chips = useMemo(() => [
|
const chips = useMemo(() => [
|
||||||
{ key: 'posture', label: '体态评估', action: () => router.push('/ai-posture-assessment') },
|
{ key: 'posture', label: '体态评估', action: () => router.push('/ai-posture-assessment') },
|
||||||
{ key: 'plan', label: 'AI制定训练计划', action: () => handleQuickPlan() },
|
{ key: 'plan', label: 'AI制定训练计划', action: () => handleQuickPlan() },
|
||||||
], [router, planDraft]);
|
{ key: 'analyze', label: '分析运动记录', action: () => handleAnalyzeRecords() },
|
||||||
|
], [router, planDraft, checkin]);
|
||||||
|
|
||||||
function scrollToEnd() {
|
function scrollToEnd() {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -109,6 +115,45 @@ export default function AICoachChatScreen() {
|
|||||||
send(prompt);
|
send(prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildTrainingSummary(): string {
|
||||||
|
const entries = Object.values(checkin?.byDate || {}) as CheckinRecord[];
|
||||||
|
if (!entries.length) return '';
|
||||||
|
const recent = entries.sort((a: any, b: any) => String(b.date).localeCompare(String(a.date))).slice(0, 14);
|
||||||
|
let totalSessions = 0;
|
||||||
|
let totalExercises = 0;
|
||||||
|
let totalCompleted = 0;
|
||||||
|
const categoryCount: Record<string, number> = {};
|
||||||
|
const exerciseCount: Record<string, number> = {};
|
||||||
|
for (const rec of recent) {
|
||||||
|
if (!rec?.items?.length) continue;
|
||||||
|
totalSessions += 1;
|
||||||
|
for (const it of rec.items) {
|
||||||
|
totalExercises += 1;
|
||||||
|
if (it.completed) totalCompleted += 1;
|
||||||
|
categoryCount[it.category] = (categoryCount[it.category] || 0) + 1;
|
||||||
|
exerciseCount[it.name] = (exerciseCount[it.name] || 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const topCategories = Object.entries(categoryCount).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([k, v]) => `${k}×${v}`);
|
||||||
|
const topExercises = Object.entries(exerciseCount).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k, v]) => `${k}×${v}`);
|
||||||
|
return [
|
||||||
|
`统计周期:最近${recent.length}天(按有记录日计 ${totalSessions} 天)`,
|
||||||
|
`记录条目:${totalExercises},完成标记:${totalCompleted}`,
|
||||||
|
topCategories.length ? `高频类别:${topCategories.join(',')}` : '',
|
||||||
|
topExercises.length ? `高频动作:${topExercises.join(',')}` : '',
|
||||||
|
].filter(Boolean).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAnalyzeRecords() {
|
||||||
|
const summary = buildTrainingSummary();
|
||||||
|
if (!summary) {
|
||||||
|
send('我还没有可分析的打卡记录,请先在“每日打卡”添加并完成一些训练记录,然后帮我分析近期训练表现与改进建议。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prompt = `请基于以下我的近期训练记录进行分析,输出:1)整体训练负荷与节奏;2)动作与肌群的均衡性(指出偏多/偏少);3)容易忽视的恢复与热身建议;4)后续一周的优化建议(频次/时长/动作方向)。\n\n${summary}`;
|
||||||
|
send(prompt);
|
||||||
|
}
|
||||||
|
|
||||||
function renderItem({ item }: { item: ChatMessage }) {
|
function renderItem({ item }: { item: ChatMessage }) {
|
||||||
const isUser = item.role === 'user';
|
const isUser = item.role === 'user';
|
||||||
return (
|
return (
|
||||||
@@ -118,9 +163,7 @@ export default function AICoachChatScreen() {
|
|||||||
style={[styles.row, { justifyContent: isUser ? 'flex-end' : 'flex-start' }]}
|
style={[styles.row, { justifyContent: isUser ? 'flex-end' : 'flex-start' }]}
|
||||||
>
|
>
|
||||||
{!isUser && (
|
{!isUser && (
|
||||||
<View style={[styles.avatar, { backgroundColor: theme.primary }]}>
|
<Image source={{ uri: COACH_AVATAR }} style={styles.avatar} />
|
||||||
<Text style={styles.avatarText}>AI</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -282,7 +325,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
inputRow: {
|
inputRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-end',
|
alignItems: 'center',
|
||||||
padding: 8,
|
padding: 8,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
@@ -292,8 +335,10 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
maxHeight: 120,
|
maxHeight: 120,
|
||||||
|
minHeight: 40,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
|
textAlignVertical: 'center',
|
||||||
},
|
},
|
||||||
sendBtn: {
|
sendBtn: {
|
||||||
width: 40,
|
width: 40,
|
||||||
|
|||||||
@@ -164,11 +164,7 @@ export default function EditProfileScreen() {
|
|||||||
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 16 }}>
|
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 16 }}>
|
||||||
<TouchableOpacity activeOpacity={0.85} onPress={pickAvatarFromLibrary}>
|
<TouchableOpacity activeOpacity={0.85} onPress={pickAvatarFromLibrary}>
|
||||||
<View style={styles.avatarCircle}>
|
<View style={styles.avatarCircle}>
|
||||||
{profile.avatarUri ? (
|
<Image source={{ uri: profile.avatarUri || 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg' }} style={styles.avatarImage} />
|
||||||
<Image source={{ uri: profile.avatarUri }} style={styles.avatarImage} />
|
|
||||||
) : (
|
|
||||||
<View style={{ width: 56, height: 56, borderRadius: 28, backgroundColor: '#D4A574' }} />
|
|
||||||
)}
|
|
||||||
<View style={styles.avatarOverlay}>
|
<View style={styles.avatarOverlay}>
|
||||||
<Ionicons name="camera" size={22} color="#192126" />
|
<Ionicons name="camera" size={22} color="#192126" />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import { ProgressBar } from '@/components/ProgressBar';
|
import { ProgressBar } from '@/components/ProgressBar';
|
||||||
|
import { useAppDispatch } from '@/hooks/redux';
|
||||||
|
import { setDailyCaloriesGoal, setDailyStepsGoal, setPilatesPurposes } from '@/store/userSlice';
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
calories: '@goal_calories_burn',
|
calories: '@goal_calories_burn',
|
||||||
@@ -32,6 +34,7 @@ export default function GoalsScreen() {
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colors = Colors[theme];
|
const colors = Colors[theme];
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [calories, setCalories] = useState<number>(400);
|
const [calories, setCalories] = useState<number>(400);
|
||||||
const [steps, setSteps] = useState<number>(8000);
|
const [steps, setSteps] = useState<number>(8000);
|
||||||
@@ -66,14 +69,17 @@ export default function GoalsScreen() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AsyncStorage.setItem(STORAGE_KEYS.calories, String(calories)).catch(() => { });
|
AsyncStorage.setItem(STORAGE_KEYS.calories, String(calories)).catch(() => { });
|
||||||
|
dispatch(setDailyCaloriesGoal(calories));
|
||||||
}, [calories]);
|
}, [calories]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AsyncStorage.setItem(STORAGE_KEYS.steps, String(steps)).catch(() => { });
|
AsyncStorage.setItem(STORAGE_KEYS.steps, String(steps)).catch(() => { });
|
||||||
|
dispatch(setDailyStepsGoal(steps));
|
||||||
}, [steps]);
|
}, [steps]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AsyncStorage.setItem(STORAGE_KEYS.purposes, JSON.stringify(purposes)).catch(() => { });
|
AsyncStorage.setItem(STORAGE_KEYS.purposes, JSON.stringify(purposes)).catch(() => { });
|
||||||
|
dispatch(setPilatesPurposes(purposes));
|
||||||
}, [purposes]);
|
}, [purposes]);
|
||||||
|
|
||||||
const caloriesPercent = useMemo(() =>
|
const caloriesPercent = useMemo(() =>
|
||||||
@@ -154,9 +160,8 @@ export default function GoalsScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: theme === 'light' ? '#F5F5F5' : colors.background }]}>
|
<View style={[styles.container, { backgroundColor: theme === 'light' ? '#F5F5F5' : colors.background }]}>
|
||||||
<HeaderBar title="目标管理" onBack={() => router.back()} withSafeTop={false} tone={theme} transparent />
|
|
||||||
|
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<SafeAreaView style={styles.safeArea}>
|
||||||
|
<HeaderBar title="目标管理" onBack={() => router.back()} withSafeTop={false} tone={theme} transparent />
|
||||||
<ScrollView contentContainerStyle={[styles.content, { paddingBottom: Math.max(20, insets.bottom + 16) }]}
|
<ScrollView contentContainerStyle={[styles.content, { paddingBottom: Math.max(20, insets.bottom + 16) }]}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type PlanCardProps = {
|
|||||||
image: string;
|
image: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
level: Level;
|
level?: Level;
|
||||||
progress: number; // 0 - 1
|
progress: number; // 0 - 1
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -17,11 +17,13 @@ export function PlanCard({ image, title, subtitle, level, progress }: PlanCardPr
|
|||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Image source={{ uri: image }} style={styles.image} />
|
<Image source={{ uri: image }} style={styles.image} />
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
|
{level && (
|
||||||
<View style={styles.badgeContainer}>
|
<View style={styles.badgeContainer}>
|
||||||
<View style={styles.badge}>
|
<View style={styles.badge}>
|
||||||
<Text style={styles.badgeText}>{level}</Text>
|
<Text style={styles.badgeText}>{level}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text style={styles.title}>{title}</Text>
|
<Text style={styles.title}>{title}</Text>
|
||||||
<Text style={styles.subtitle}>{subtitle}</Text>
|
<Text style={styles.subtitle}>{subtitle}</Text>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export type UserProfile = {
|
|||||||
weightKg?: number;
|
weightKg?: number;
|
||||||
heightCm?: number;
|
heightCm?: number;
|
||||||
avatarUri?: string | null;
|
avatarUri?: string | null;
|
||||||
|
dailyStepsGoal?: number; // 每日步数目标(用于 Explore 页等)
|
||||||
|
dailyCaloriesGoal?: number; // 每日卡路里消耗目标
|
||||||
|
pilatesPurposes?: string[]; // 普拉提目的(多选)
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserState = {
|
export type UserState = {
|
||||||
@@ -21,10 +24,12 @@ export type UserState = {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_MEMBER_NAME = '普拉提星球学员';
|
||||||
|
|
||||||
const initialState: UserState = {
|
const initialState: UserState = {
|
||||||
token: null,
|
token: null,
|
||||||
profile: {
|
profile: {
|
||||||
|
fullName: DEFAULT_MEMBER_NAME,
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -94,6 +99,15 @@ const userSlice = createSlice({
|
|||||||
updateProfile(state, action: PayloadAction<Partial<UserProfile>>) {
|
updateProfile(state, action: PayloadAction<Partial<UserProfile>>) {
|
||||||
state.profile = { ...(state.profile ?? {}), ...action.payload };
|
state.profile = { ...(state.profile ?? {}), ...action.payload };
|
||||||
},
|
},
|
||||||
|
setDailyStepsGoal(state, action: PayloadAction<number>) {
|
||||||
|
state.profile = { ...(state.profile ?? {}), dailyStepsGoal: action.payload };
|
||||||
|
},
|
||||||
|
setDailyCaloriesGoal(state, action: PayloadAction<number>) {
|
||||||
|
state.profile = { ...(state.profile ?? {}), dailyCaloriesGoal: action.payload };
|
||||||
|
},
|
||||||
|
setPilatesPurposes(state, action: PayloadAction<string[]>) {
|
||||||
|
state.profile = { ...(state.profile ?? {}), pilatesPurposes: action.payload };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
@@ -102,6 +116,9 @@ const userSlice = createSlice({
|
|||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.token = action.payload.token;
|
state.token = action.payload.token;
|
||||||
state.profile = action.payload.profile;
|
state.profile = action.payload.profile;
|
||||||
|
if (!state.profile?.fullName || !state.profile.fullName.trim()) {
|
||||||
|
state.profile.fullName = DEFAULT_MEMBER_NAME;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(login.rejected, (state, action) => {
|
.addCase(login.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
@@ -110,6 +127,9 @@ 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;
|
||||||
|
if (!state.profile?.fullName || !state.profile.fullName.trim()) {
|
||||||
|
state.profile.fullName = DEFAULT_MEMBER_NAME;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(logout.fulfilled, (state) => {
|
.addCase(logout.fulfilled, (state) => {
|
||||||
state.token = null;
|
state.token = null;
|
||||||
@@ -118,7 +138,7 @@ const userSlice = createSlice({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { updateProfile } = userSlice.actions;
|
export const { updateProfile, setDailyStepsGoal, setDailyCaloriesGoal, setPilatesPurposes } = userSlice.actions;
|
||||||
export default userSlice.reducer;
|
export default userSlice.reducer;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user