feat: 更新依赖项并优化组件结构
- 在 package.json 和 package-lock.json 中新增 @sentry/react-native、react-native-device-info 和 react-native-purchases 依赖 - 更新统计页面,替换 CircularRing 组件为 FitnessRingsCard,增强健身数据展示 - 在布局文件中引入 ToastProvider,优化用户通知体验 - 新增 SuccessToast 组件,提供全局成功提示功能 - 更新健康数据获取逻辑,支持健身圆环数据的提取 - 优化多个组件的样式和交互,提升用户体验
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||
import { BMICard } from '@/components/BMICard';
|
||||
import { CircularRing } from '@/components/CircularRing';
|
||||
import { FitnessRingsCard } from '@/components/FitnessRingsCard';
|
||||
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
|
||||
import { ProgressBar } from '@/components/ProgressBar';
|
||||
import { StressMeter } from '@/components/StressMeter';
|
||||
@@ -52,13 +52,21 @@ export default function ExploreScreen() {
|
||||
// 日期条自动滚动到选中项
|
||||
const daysScrollRef = useRef<import('react-native').ScrollView | null>(null);
|
||||
const [scrollWidth, setScrollWidth] = useState(0);
|
||||
const DAY_PILL_WIDTH = 68;
|
||||
const DAY_PILL_SPACING = 12;
|
||||
const DAY_PILL_WIDTH = 48;
|
||||
const DAY_PILL_SPACING = 8;
|
||||
|
||||
const scrollToIndex = (index: number, animated = true) => {
|
||||
const baseOffset = index * (DAY_PILL_WIDTH + DAY_PILL_SPACING);
|
||||
if (!daysScrollRef.current || scrollWidth === 0) return;
|
||||
|
||||
const itemWidth = DAY_PILL_WIDTH + DAY_PILL_SPACING;
|
||||
const baseOffset = index * itemWidth;
|
||||
const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2));
|
||||
daysScrollRef.current?.scrollTo({ x: centerOffset, animated });
|
||||
|
||||
// 确保不会滚动超出边界
|
||||
const maxScrollOffset = Math.max(0, (days.length * itemWidth) - scrollWidth);
|
||||
const finalOffset = Math.min(centerOffset, maxScrollOffset);
|
||||
|
||||
daysScrollRef.current.scrollTo({ x: finalOffset, animated });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -68,6 +76,14 @@ export default function ExploreScreen() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [scrollWidth]);
|
||||
|
||||
// 当选中索引变化时,滚动到对应位置
|
||||
useEffect(() => {
|
||||
if (scrollWidth > 0) {
|
||||
scrollToIndex(selectedIndex, true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedIndex]);
|
||||
|
||||
// HealthKit: 每次页面聚焦都拉取今日数据
|
||||
const [stepCount, setStepCount] = useState<number | null>(null);
|
||||
const [activeCalories, setActiveCalories] = useState<number | null>(null);
|
||||
@@ -76,6 +92,15 @@ export default function ExploreScreen() {
|
||||
// HRV数据
|
||||
const [hrvValue, setHrvValue] = useState<number>(0);
|
||||
const [hrvUpdateTime, setHrvUpdateTime] = useState<Date>(new Date());
|
||||
// 健身圆环数据
|
||||
const [fitnessRingsData, setFitnessRingsData] = useState({
|
||||
activeCalories: 0,
|
||||
activeCaloriesGoal: 350,
|
||||
exerciseMinutes: 0,
|
||||
exerciseMinutesGoal: 30,
|
||||
standHours: 0,
|
||||
standHoursGoal: 12
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// 用于触发动画重置的 token(当日期或数据变化时更新)
|
||||
@@ -124,6 +149,15 @@ export default function ExploreScreen() {
|
||||
setStepCount(data.steps);
|
||||
setActiveCalories(Math.round(data.activeEnergyBurned));
|
||||
setSleepDuration(data.sleepDuration);
|
||||
// 更新健身圆环数据
|
||||
setFitnessRingsData({
|
||||
activeCalories: data.activeCalories,
|
||||
activeCaloriesGoal: data.activeCaloriesGoal,
|
||||
exerciseMinutes: data.exerciseMinutes,
|
||||
exerciseMinutesGoal: data.exerciseMinutesGoal,
|
||||
standHours: data.standHours,
|
||||
standHoursGoal: data.standHoursGoal
|
||||
});
|
||||
|
||||
const hrv = data.hrv ?? 0;
|
||||
setHrvValue(hrv);
|
||||
@@ -195,7 +229,6 @@ export default function ExploreScreen() {
|
||||
// 日期点击时,加载对应日期数据
|
||||
const onSelectDate = (index: number) => {
|
||||
setSelectedIndex(index);
|
||||
scrollToIndex(index);
|
||||
const target = days[index]?.date?.toDate();
|
||||
if (target) {
|
||||
loadHealthData(target);
|
||||
@@ -320,19 +353,17 @@ export default function ExploreScreen() {
|
||||
// compact={true}
|
||||
/>
|
||||
|
||||
<View style={[styles.masonryCard, styles.trainingCard]}>
|
||||
<Text style={styles.cardTitleSecondary}>训练时间</Text>
|
||||
<View style={styles.trainingContent}>
|
||||
<CircularRing
|
||||
size={120}
|
||||
strokeWidth={12}
|
||||
trackColor="#E2D9FD"
|
||||
progressColor="#8B74F3"
|
||||
progress={trainingProgress}
|
||||
resetToken={animToken}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FitnessRingsCard
|
||||
activeCalories={fitnessRingsData.activeCalories}
|
||||
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
|
||||
exerciseMinutes={fitnessRingsData.exerciseMinutes}
|
||||
exerciseMinutesGoal={fitnessRingsData.exerciseMinutesGoal}
|
||||
standHours={fitnessRingsData.standHours}
|
||||
standHoursGoal={fitnessRingsData.standHoursGoal}
|
||||
resetToken={animToken}
|
||||
style={styles.masonryCard}
|
||||
/>
|
||||
|
||||
|
||||
<View style={[styles.masonryCard, styles.sleepCard]}>
|
||||
<View style={styles.cardHeaderRow}>
|
||||
@@ -396,13 +427,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
dayItemWrapper: {
|
||||
alignItems: 'center',
|
||||
width: 68,
|
||||
marginRight: 12,
|
||||
width: 48,
|
||||
marginRight: 8,
|
||||
},
|
||||
dayPill: {
|
||||
width: 68,
|
||||
height: 68,
|
||||
borderRadius: 18,
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@@ -413,16 +444,16 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: lightColors.datePickerSelected,
|
||||
},
|
||||
dayLabel: {
|
||||
fontSize: 16,
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
color: '#192126',
|
||||
marginBottom: 2,
|
||||
marginBottom: 1,
|
||||
},
|
||||
dayLabelSelected: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
dayDate: {
|
||||
fontSize: 16,
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
},
|
||||
@@ -430,12 +461,12 @@ const styles = StyleSheet.create({
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
selectedDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: 2.5,
|
||||
backgroundColor: lightColors.datePickerSelected,
|
||||
marginTop: 10,
|
||||
marginBottom: 4,
|
||||
marginTop: 6,
|
||||
marginBottom: 2,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
sectionTitle: {
|
||||
@@ -481,13 +512,13 @@ const styles = StyleSheet.create({
|
||||
|
||||
cardTitleSecondary: {
|
||||
color: '#9AA3AE',
|
||||
fontSize: 14,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
marginBottom: 10,
|
||||
},
|
||||
caloriesValue: {
|
||||
color: '#192126',
|
||||
fontSize: 22,
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
},
|
||||
trainingContent: {
|
||||
@@ -569,8 +600,8 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 12,
|
||||
},
|
||||
iconSquare: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#FFFFFF',
|
||||
alignItems: 'center',
|
||||
@@ -578,7 +609,7 @@ const styles = StyleSheet.create({
|
||||
marginRight: 10,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 18,
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
},
|
||||
@@ -606,7 +637,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: '#FFE4B8',
|
||||
},
|
||||
stepsValue: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
color: '#7A6A42',
|
||||
fontWeight: '700',
|
||||
marginBottom: 8,
|
||||
|
||||
@@ -12,9 +12,9 @@ import { store } from '@/store';
|
||||
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
||||
import React from 'react';
|
||||
import RNExitApp from 'react-native-exit-app';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { DialogProvider } from '@/components/ui/DialogProvider';
|
||||
import { ToastProvider } from '@/contexts/ToastContext';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
@@ -58,7 +58,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
onAgree={handlePrivacyAgree}
|
||||
onDisagree={handlePrivacyDisagree}
|
||||
/>
|
||||
<Toast />
|
||||
</DialogProvider>
|
||||
);
|
||||
}
|
||||
@@ -77,27 +76,28 @@ export default function RootLayout() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Bootstrapper>
|
||||
<ThemeProvider value={DefaultTheme}>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="onboarding" />
|
||||
<Stack.Screen name="(tabs)" />
|
||||
<Stack.Screen name="challenge" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="training-plan" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="workout" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="profile/edit" />
|
||||
<Stack.Screen name="profile/goals" options={{ headerShown: false }} />
|
||||
<ToastProvider>
|
||||
<ThemeProvider value={DefaultTheme}>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="onboarding" />
|
||||
<Stack.Screen name="(tabs)" />
|
||||
<Stack.Screen name="challenge" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="training-plan" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="workout" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="profile/edit" />
|
||||
<Stack.Screen name="profile/goals" options={{ headerShown: false }} />
|
||||
|
||||
<Stack.Screen name="ai-posture-assessment" />
|
||||
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
||||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="nutrition/records" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<StatusBar style="dark" />
|
||||
<Toast />
|
||||
</ThemeProvider>
|
||||
<Stack.Screen name="ai-posture-assessment" />
|
||||
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
||||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="nutrition/records" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<StatusBar style="dark" />
|
||||
</ThemeProvider>
|
||||
</ToastProvider>
|
||||
</Bootstrapper>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@@ -43,21 +43,37 @@ export default function NutritionRecordsScreen() {
|
||||
// 日期滚动相关
|
||||
const daysScrollRef = useRef<ScrollView | null>(null);
|
||||
const [scrollWidth, setScrollWidth] = useState(0);
|
||||
const DAY_PILL_WIDTH = 68;
|
||||
const DAY_PILL_SPACING = 12;
|
||||
const DAY_PILL_WIDTH = 60; // 48px width + 12px marginRight = 60px total per item
|
||||
const DAY_PILL_SPACING = 0; // spacing is included in the width above
|
||||
|
||||
// 日期滚动控制
|
||||
const scrollToIndex = (index: number, animated = true) => {
|
||||
const baseOffset = index * (DAY_PILL_WIDTH + DAY_PILL_SPACING);
|
||||
const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2));
|
||||
if (scrollWidth <= 0) return;
|
||||
|
||||
const itemOffset = index * DAY_PILL_WIDTH;
|
||||
const scrollViewCenterX = scrollWidth / 2;
|
||||
const itemCenterX = DAY_PILL_WIDTH / 2;
|
||||
const centerOffset = Math.max(0, itemOffset - scrollViewCenterX + itemCenterX);
|
||||
|
||||
daysScrollRef.current?.scrollTo({ x: centerOffset, animated });
|
||||
};
|
||||
|
||||
// 初始化时滚动到选中位置
|
||||
useEffect(() => {
|
||||
if (scrollWidth > 0) {
|
||||
scrollToIndex(selectedIndex, false);
|
||||
// 延迟滚动以确保ScrollView已经完全渲染
|
||||
setTimeout(() => {
|
||||
scrollToIndex(selectedIndex, false);
|
||||
}, 100);
|
||||
}
|
||||
}, [scrollWidth, selectedIndex]);
|
||||
}, [scrollWidth]);
|
||||
|
||||
// 选中日期变化时滚动
|
||||
useEffect(() => {
|
||||
if (scrollWidth > 0) {
|
||||
scrollToIndex(selectedIndex, true);
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
// 加载记录数据
|
||||
const loadRecords = async (isRefresh = false, loadMore = false) => {
|
||||
@@ -194,7 +210,6 @@ export default function NutritionRecordsScreen() {
|
||||
onPress={() => {
|
||||
if (!isDisabled) {
|
||||
setSelectedIndex(index);
|
||||
scrollToIndex(index);
|
||||
}
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
|
||||
Reference in New Issue
Block a user