feat(ui): 统一健康卡片标题图标并优化语音录音稳定性

- 为所有健康数据卡片添加对应功能图标,提升视觉一致性
- 将“小鱼干”文案统一为“能量值”,并更新获取说明
- 语音录音页面增加组件卸载保护、错误提示与资源清理逻辑
- 个人页支持毛玻璃按钮样式,默认用户名置空
- 新增血氧、饮食、心情、压力、睡眠、步数、体重等图标资源
- 升级 react-native-purchases 至 9.4.3
- 移除 useAuthGuard 调试日志
This commit is contained in:
richarjiang
2025-09-16 09:35:50 +08:00
parent 42b6b2076c
commit 63ed820e93
26 changed files with 359 additions and 103 deletions

View File

@@ -512,7 +512,7 @@ export default function GoalsScreen() {
text: '每日目标通知', text: '每日目标通知',
onPress: async () => { onPress: async () => {
try { try {
const userName = userProfile?.name || '小海豹'; const userName = userProfile?.name || '';
const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications(
{ {
title: '每日运动目标', title: '每日运动目标',
@@ -533,7 +533,7 @@ export default function GoalsScreen() {
text: '每周目标通知', text: '每周目标通知',
onPress: async () => { onPress: async () => {
try { try {
const userName = userProfile?.name || '小海豹'; const userName = userProfile?.name || '';
const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications(
{ {
title: '每周运动目标', title: '每周运动目标',
@@ -557,7 +557,7 @@ export default function GoalsScreen() {
text: '目标达成通知', text: '目标达成通知',
onPress: async () => { onPress: async () => {
try { try {
const userName = userProfile?.name || '小海豹'; const userName = userProfile?.name || '';
await GoalNotificationHelpers.sendGoalAchievementNotification(userName, '每日运动目标'); await GoalNotificationHelpers.sendGoalAchievementNotification(userName, '每日运动目标');
Alert.alert('成功', '目标达成通知已发送'); Alert.alert('成功', '目标达成通知已发送');
} catch (error) { } catch (error) {

View File

@@ -6,12 +6,14 @@ import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useNotifications } from '@/hooks/useNotifications'; import { useNotifications } from '@/hooks/useNotifications';
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice'; import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
import { getItem, setItem } from '@/utils/kvStore';
import { log } from '@/utils/logger'; import { log } from '@/utils/logger';
import { getNotificationEnabled, setNotificationEnabled as saveNotificationEnabled } from '@/utils/userPreferences'; import { getNotificationEnabled, setNotificationEnabled as saveNotificationEnabled } from '@/utils/userPreferences';
import { getItem, setItem } from '@/utils/kvStore'; import { Button, Host, Text as SwiftText } from '@expo/ui/swift-ui';
import { frame, glassEffect } from '@expo/ui/swift-ui/modifiers';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import { isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
@@ -25,6 +27,7 @@ export default function PersonalScreen() {
const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard(); const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const isLgAvaliable = isLiquidGlassAvailable()
// 推送通知相关 // 推送通知相关
const { const {
@@ -212,9 +215,35 @@ export default function PersonalScreen() {
<Text style={styles.userName}>{displayName}</Text> <Text style={styles.userName}>{displayName}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}> {isLgAvaliable ? <Host style={{
marginRight: 18,
}}>
<Button
variant='default'
onPress={() => {
console.log(111111);
// pushIfAuthedElseLogin('/profile/edit')
}}
modifiers={[
frame({
width: 60,
height: 30,
}),
glassEffect({
glass: {
variant: 'regular',
interactive: true
}
})
]} >
<SwiftText size={14} color='black' weight={'medium'}></SwiftText>
</Button>
</Host> : <TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
<Text style={styles.editButtonText}></Text> <Text style={styles.editButtonText}></Text>
</TouchableOpacity> </TouchableOpacity>}
</View> </View>
</View> </View>
@@ -381,7 +410,7 @@ export default function PersonalScreen() {
transition={200} transition={200}
cachePolicy="memory-disk" cachePolicy="memory-disk"
/> */} /> */}
<Text style={styles.fishRecordText}></Text> <Text style={styles.fishRecordText}></Text>
</View> </View>
<ActivityHeatMap /> <ActivityHeatMap />
{menuSections.map((section, index) => ( {menuSections.map((section, index) => (

View File

@@ -9,7 +9,7 @@ import { Ionicons } from '@expo/vector-icons';
import Voice from '@react-native-voice/voice'; import Voice from '@react-native-voice/voice';
import { BlurView } from 'expo-blur'; import { BlurView } from 'expo-blur';
import { router, useLocalSearchParams } from 'expo-router'; import { router, useLocalSearchParams } from 'expo-router';
import React, { useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { import {
Alert, Alert,
Animated, Animated,
@@ -33,6 +33,10 @@ export default function VoiceRecordScreen() {
const [isListening, setIsListening] = useState(false); const [isListening, setIsListening] = useState(false);
const [analysisProgress, setAnalysisProgress] = useState(0); const [analysisProgress, setAnalysisProgress] = useState(0);
// 用于跟踪组件是否已卸载,防止在组件卸载后设置状态
const isMountedRef = useRef(true);
const progressIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// 动画相关 // 动画相关
const scaleAnimation = useRef(new Animated.Value(1)).current; const scaleAnimation = useRef(new Animated.Value(1)).current;
const pulseAnimation = useRef(new Animated.Value(1)).current; const pulseAnimation = useRef(new Animated.Value(1)).current;
@@ -108,50 +112,78 @@ export default function VoiceRecordScreen() {
progressAnimation.setValue(0); progressAnimation.setValue(0);
}; };
// 语音识别回调 // 语音识别回调 - 使用 useCallback 避免每次渲染重新创建
const onSpeechStart = () => { const onSpeechStart = useCallback(() => {
console.log('语音开始');
if (!isMountedRef.current) return;
setIsListening(true); setIsListening(true);
setRecordState('listening'); setRecordState('listening');
startPulseAnimation(); startPulseAnimation();
startWaveAnimation(); startWaveAnimation();
}; }, []);
const onSpeechRecognized = () => { const onSpeechRecognized = useCallback(() => {
console.log('语音识别中...'); console.log('语音识别中...');
}; }, []);
const onSpeechEnd = useCallback(() => {
console.log('语音结束');
if (!isMountedRef.current) return;
const onSpeechEnd = () => {
setIsListening(false); setIsListening(false);
setRecordState('processing'); setRecordState('processing');
stopAnimations(); stopAnimations();
}; }, []);
const onSpeechError = (error: any) => { const onSpeechError = useCallback((error: any) => {
console.log('语音识别错误:', error); console.log('语音识别错误:', error);
if (!isMountedRef.current) return;
setIsListening(false); setIsListening(false);
setRecordState('idle'); setRecordState('idle');
stopAnimations(); stopAnimations();
};
const onSpeechResults = (event: any) => { // 显示更友好的错误信息
if (error.error?.code === '7') {
Alert.alert('提示', '没有检测到语音输入,请重试');
} else if (error.error?.code === '2') {
Alert.alert('提示', '网络连接异常,请检查网络后重试');
} else {
Alert.alert('提示', '语音识别出现问题,请重试');
}
}, []);
const onSpeechResults = useCallback((event: any) => {
console.log('语音识别结果:', event);
if (!isMountedRef.current) return;
const text = event.value?.[0] || ''; const text = event.value?.[0] || '';
setRecognizedText(text); if (text.trim()) {
setRecordState('result'); setRecognizedText(text);
setRecordState('result');
} else {
setRecordState('idle');
Alert.alert('提示', '未识别到有效内容,请重新录音');
}
stopAnimations(); stopAnimations();
}; }, []);
const onSpeechPartialResults = useCallback((event: any) => {
if (!isMountedRef.current) return;
const onSpeechPartialResults = (event: any) => {
const text = event.value?.[0] || ''; const text = event.value?.[0] || '';
setRecognizedText(text); setRecognizedText(text);
}; }, []);
const onSpeechVolumeChanged = useCallback((event: any) => {
if (!isMountedRef.current) return;
const onSpeechVolumeChanged = (event: any) => {
// 根据音量调整动画 // 根据音量调整动画
const volume = event.value || 0; const volume = event.value || 0;
const scale = 1 + (volume * 0.1); const scale = 1 + (volume * 0.1);
scaleAnimation.setValue(Math.min(scale, 1.5)); scaleAnimation.setValue(Math.min(scale, 1.5));
}; }, []);
useEffect(() => { useEffect(() => {
// 初始化语音识别 // 初始化语音识别
@@ -164,57 +196,91 @@ export default function VoiceRecordScreen() {
Voice.onSpeechVolumeChanged = onSpeechVolumeChanged; Voice.onSpeechVolumeChanged = onSpeechVolumeChanged;
return () => { return () => {
Voice.destroy().then(Voice.removeAllListeners); // 标记组件已卸载
isMountedRef.current = false;
// 清理进度定时器
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
// 清理语音识别资源
const cleanup = async () => {
try {
await Voice.stop();
await Voice.destroy();
Voice.removeAllListeners();
} catch (error) {
console.log('清理语音识别资源失败:', error);
}
};
cleanup();
}; };
}, [onSpeechStart, onSpeechRecognized, onSpeechEnd, onSpeechError, onSpeechResults, onSpeechPartialResults, onSpeechVolumeChanged]); }, [onSpeechStart, onSpeechRecognized, onSpeechEnd, onSpeechError, onSpeechResults, onSpeechPartialResults, onSpeechVolumeChanged]);
// 开始录音 // 开始录音
const startRecording = async () => { const startRecording = async () => {
try { try {
// 重置状态
setRecognizedText(''); setRecognizedText('');
setRecordState('idle');
triggerHapticFeedback('impactMedium'); triggerHapticFeedback('impactMedium');
await Voice.start('zh-CN'); // 设置为中文识别 // 确保之前的识别已停止
await Voice.stop();
// 添加短暂延迟确保资源清理完成
await new Promise(resolve => setTimeout(resolve, 100));
// 启动新的语音识别
await Voice.start('zh-CN');
} catch (error) { } catch (error) {
console.log('启动语音识别失败:', error); console.log('启动语音识别失败:', error);
setRecordState('idle'); setRecordState('idle');
Alert.alert('录音失败', '无法启动语音识别,请检查权限设置'); setIsListening(false);
Alert.alert('录音失败', '无法启动语音识别,请检查麦克风权限设置');
} }
}; };
// 停止录音 // 停止录音
const stopRecording = async () => { const stopRecording = async () => {
try { try {
console.log('停止录音');
setIsListening(false);
await Voice.stop(); await Voice.stop();
triggerHapticFeedback('impactLight'); triggerHapticFeedback('impactLight');
} catch (error) { } catch (error) {
console.log('停止语音识别失败:', error); console.log('停止语音识别失败:', error);
setIsListening(false);
setRecordState('idle');
} }
}; };
// 重新录音 // 重新录音
const retryRecording = async () => { const retryRecording = async () => {
// 停止所有动画
stopAnimations();
// 重置所有状态
setRecognizedText('');
setAnalysisProgress(0);
setIsListening(false);
setRecordState('idle');
// 确保语音识别已停止
try { try {
await Voice.stop(); // 停止所有动画
} catch { stopAnimations();
// 忽略停止错误,可能已经停止了
}
// 延迟一点再开始新的录音,确保状态完全重置 // 重置所有状态
setTimeout(() => { setRecognizedText('');
startRecording(); setAnalysisProgress(0);
}, 100); setIsListening(false);
setRecordState('idle');
// 确保语音识别已停止
await Voice.stop();
// 延迟一点再开始新的录音,确保状态完全重置
setTimeout(() => {
startRecording();
}, 200);
} catch (error) {
console.log('重新录音失败:', error);
setRecordState('idle');
setIsListening(false);
}
}; };
// 确认并分析食物文本 // 确认并分析食物文本
@@ -233,10 +299,19 @@ export default function VoiceRecordScreen() {
startAnalysisAnimation(); startAnalysisAnimation();
// 模拟进度更新 // 模拟进度更新
const progressInterval = setInterval(() => { progressIntervalRef.current = setInterval(() => {
if (!isMountedRef.current) {
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
return;
}
setAnalysisProgress(prev => { setAnalysisProgress(prev => {
if (prev >= 90) { if (prev >= 90) {
clearInterval(progressInterval); if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
return prev; return prev;
} }
return prev + Math.random() * 15; return prev + Math.random() * 15;
@@ -248,7 +323,12 @@ export default function VoiceRecordScreen() {
const result = await analyzeFoodFromText({ text: recognizedText }); const result = await analyzeFoodFromText({ text: recognizedText });
// 清理进度定时器 // 清理进度定时器
clearInterval(progressInterval); if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
if (!isMountedRef.current) return;
setAnalysisProgress(100); setAnalysisProgress(100);
// 生成识别结果ID并保存到Redux // 生成识别结果ID并保存到Redux
@@ -272,8 +352,17 @@ export default function VoiceRecordScreen() {
} catch (error) { } catch (error) {
console.error('食物分析失败:', error); console.error('食物分析失败:', error);
// 清理进度定时器
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
if (!isMountedRef.current) return;
stopAnimations(); stopAnimations();
setRecordState('result'); setRecordState('result');
dispatch(setLoading(false));
const errorMessage = error instanceof Error ? error.message : '分析失败,请重试'; const errorMessage = error instanceof Error ? error.message : '分析失败,请重试';
dispatch(setError(errorMessage)); dispatch(setError(errorMessage));
@@ -281,11 +370,24 @@ export default function VoiceRecordScreen() {
} }
}; };
const handleBack = () => { const handleBack = async () => {
if (isListening) { try {
stopRecording(); // 如果正在录音,先停止
if (isListening) {
await stopRecording();
}
// 停止所有动画
stopAnimations();
// 确保语音识别完全停止
await Voice.stop();
router.back();
} catch (error) {
console.log('返回时清理资源失败:', error);
router.back();
} }
router.back();
}; };
// 获取状态对应的UI文本 // 获取状态对应的UI文本

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -184,23 +184,23 @@ const ActivityHeatMap = () => {
> >
<View style={[styles.popoverContent, { backgroundColor: colors.card }]}> <View style={[styles.popoverContent, { backgroundColor: colors.card }]}>
<Text style={[styles.popoverTitle, { color: colors.text }]}> <Text style={[styles.popoverTitle, { color: colors.text }]}>
AI
</Text> </Text>
<Text style={[styles.popoverSubtitle, { color: colors.text }]}> <Text style={[styles.popoverSubtitle, { color: colors.text }]}>
</Text> </Text>
<View style={styles.popoverList}> <View style={styles.popoverList}>
<Text style={[styles.popoverItem, { color: colors.textMuted }]}> <Text style={[styles.popoverItem, { color: colors.textMuted }]}>
1. +1 1. +1
</Text> </Text>
<Text style={[styles.popoverItem, { color: colors.textMuted }]}> <Text style={[styles.popoverItem, { color: colors.textMuted }]}>
2. +1 2. +1
</Text> </Text>
<Text style={[styles.popoverItem, { color: colors.textMuted }]}> <Text style={[styles.popoverItem, { color: colors.textMuted }]}>
3. +1 3. +1
</Text> </Text>
<Text style={[styles.popoverItem, { color: colors.textMuted }]}> <Text style={[styles.popoverItem, { color: colors.textMuted }]}>
4. +1 4. +1
</Text> </Text>
</View> </View>
</View> </View>

View File

@@ -1,4 +1,5 @@
import { AnimatedNumber } from '@/components/AnimatedNumber'; import { AnimatedNumber } from '@/components/AnimatedNumber';
import { Image } from 'expo-image';
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
@@ -35,6 +36,10 @@ export function BasalMetabolismCard({ value, resetToken, style }: BasalMetabolis
{/* 头部区域 */} {/* 头部区域 */}
<View style={styles.header}> <View style={styles.header}>
<View style={styles.leftSection}> <View style={styles.leftSection}>
<Image
source={require('@/assets/images/icons/icon-fire.png')}
style={styles.titleIcon}
/>
<Text style={styles.title}></Text> <Text style={styles.title}></Text>
</View> </View>
<View style={[styles.statusBadge, { backgroundColor: status.color + '20' }]}> <View style={[styles.statusBadge, { backgroundColor: status.color + '20' }]}>
@@ -117,6 +122,12 @@ const styles = StyleSheet.create({
title: { title: {
fontSize: 14, fontSize: 14,
color: '#0F172A', color: '#0F172A',
fontWeight: '600',
},
titleIcon: {
width: 16,
height: 16,
marginRight: 4,
}, },
statusBadge: { statusBadge: {
paddingHorizontal: 8, paddingHorizontal: 8,

View File

@@ -2,7 +2,7 @@ import { MoodCheckin, getMoodConfig } from '@/services/moodCheckins';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import LottieView from 'lottie-react-native'; import LottieView from 'lottie-react-native';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface MoodCardProps { interface MoodCardProps {
moodCheckin: MoodCheckin | null; moodCheckin: MoodCheckin | null;
@@ -23,7 +23,13 @@ export function MoodCard({ moodCheckin, onPress }: MoodCardProps) {
return ( return (
<TouchableOpacity onPress={onPress} style={styles.moodCardContent} > <TouchableOpacity onPress={onPress} style={styles.moodCardContent} >
<View style={styles.moodCardHeader}> <View style={styles.moodCardHeader}>
<Text style={styles.cardTitle}></Text> <View style={styles.titleContainer}>
<Image
source={require('@/assets/images/icons/icon-mood.png')}
style={styles.titleIcon}
/>
<Text style={styles.cardTitle}></Text>
</View>
<LottieView <LottieView
ref={animationRef} ref={animationRef}
source={require('@/assets/lottie/mood/mood_demo.json')} source={require('@/assets/lottie/mood/mood_demo.json')}
@@ -59,9 +65,22 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
}, },
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
},
titleIcon: {
width: 16,
height: 16,
marginRight: 6,
resizeMode: 'contain',
},
cardTitle: { cardTitle: {
fontSize: 14, fontSize: 14,
color: '#192126', color: '#192126',
fontWeight: '600'
}, },
lottieAnimation: { lottieAnimation: {

View File

@@ -140,7 +140,13 @@ export function NutritionRadarCard({
return ( return (
<TouchableOpacity style={styles.card} onPress={handleNavigateToRecords} activeOpacity={0.8}> <TouchableOpacity style={styles.card} onPress={handleNavigateToRecords} activeOpacity={0.8}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<Text style={styles.cardTitle}></Text> <View style={styles.titleContainer}>
<Image
source={require('@/assets/images/icons/icon-healthy-diet.png')}
style={styles.titleIcon}
/>
<Text style={styles.cardTitle}></Text>
</View>
<Text style={styles.cardSubtitle}>: {dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}</Text> <Text style={styles.cardSubtitle}>: {dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}</Text>
</View> </View>
@@ -292,9 +298,20 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
marginBottom: 8, marginBottom: 8,
}, },
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
},
titleIcon: {
width: 16,
height: 16,
marginRight: 6,
resizeMode: 'contain',
},
cardTitle: { cardTitle: {
fontSize: 14, fontSize: 14,
color: '#192126', color: '#192126',
fontWeight: '600'
}, },
cardSubtitle: { cardSubtitle: {
fontSize: 10, fontSize: 10,

View File

@@ -10,9 +10,10 @@ import {
import { fetchHourlyStepSamples, fetchStepCount, HourlyStepData } from '@/utils/health'; import { fetchHourlyStepSamples, fetchStepCount, HourlyStepData } from '@/utils/health';
import { logger } from '@/utils/logger'; import { logger } from '@/utils/logger';
import dayjs from 'dayjs';
import { Image } from 'expo-image';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { AnimatedNumber } from './AnimatedNumber'; import { AnimatedNumber } from './AnimatedNumber';
import dayjs from 'dayjs';
// 使用原生View来替代SVG避免导入问题 // 使用原生View来替代SVG避免导入问题
// import Svg, { Rect } from 'react-native-svg'; // import Svg, { Rect } from 'react-native-svg';
@@ -110,6 +111,10 @@ const StepsCard: React.FC<StepsCardProps> = ({
<> <>
{/* 标题和步数显示 */} {/* 标题和步数显示 */}
<View style={styles.header}> <View style={styles.header}>
<Image
source={require('@/assets/images/icons/icon-step.png')}
style={styles.titleIcon}
/>
<Text style={styles.title}></Text> <Text style={styles.title}></Text>
</View> </View>
@@ -213,12 +218,19 @@ const styles = StyleSheet.create({
}, },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'flex-start',
alignItems: 'center', alignItems: 'center',
}, },
titleIcon: {
width: 16,
height: 16,
marginRight: 6,
resizeMode: 'contain',
},
title: { title: {
fontSize: 14, fontSize: 14,
color: '#192126', color: '#192126',
fontWeight: '600'
}, },
footprintIcons: { footprintIcons: {
flexDirection: 'row', flexDirection: 'row',
@@ -228,6 +240,7 @@ const styles = StyleSheet.create({
chartContainer: { chartContainer: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
marginTop: 6
}, },
chartWrapper: { chartWrapper: {
width: '100%', width: '100%',

View File

@@ -1,5 +1,6 @@
import { fetchHRVForDate } from '@/utils/health'; import { fetchHRVForDate } from '@/utils/health';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
@@ -96,6 +97,10 @@ export function StressMeter({ curDate }: StressMeterProps) {
{/* 头部区域 */} {/* 头部区域 */}
<View style={styles.header}> <View style={styles.header}>
<View style={styles.leftSection}> <View style={styles.leftSection}>
<Image
source={require('@/assets/images/icons/icon-pressure.png')}
style={styles.titleIcon}
/>
<Text style={styles.title}></Text> <Text style={styles.title}></Text>
</View> </View>
{/* {updateTime && ( {/* {updateTime && (
@@ -172,9 +177,16 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
marginRight: 6, marginRight: 6,
}, },
titleIcon: {
width: 16,
height: 16,
marginRight: 6,
resizeMode: 'contain',
},
title: { title: {
fontSize: 14, fontSize: 14,
color: '#192126', color: '#192126',
fontWeight: '600'
}, },
valueSection: { valueSection: {
flexDirection: 'row', flexDirection: 'row',

View File

@@ -4,6 +4,7 @@ import { getQuickWaterAmount } from '@/utils/userPreferences';
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 * as Haptics from 'expo-haptics';
import { Image } from 'expo-image';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import LottieView from 'lottie-react-native'; import LottieView from 'lottie-react-native';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -185,7 +186,16 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
{/* 标题和加号按钮 */} {/* 标题和加号按钮 */}
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.title}></Text> <View style={{
flexDirection: 'row',
alignItems: 'center',
}}>
<Image
source={require('@/assets/images/icons/IconGlass.png')}
style={styles.titleIcon}
/>
<Text style={styles.title}></Text>
</View>
{isToday && ( {isToday && (
<TouchableOpacity style={styles.addButton} onPress={handleQuickAddWater}> <TouchableOpacity style={styles.addButton} onPress={handleQuickAddWater}>
<Text style={styles.addButtonText}>+ {quickWaterAmount}ml</Text> <Text style={styles.addButtonText}>+ {quickWaterAmount}ml</Text>
@@ -291,10 +301,16 @@ const styles = StyleSheet.create({
marginBottom: 4, marginBottom: 4,
minHeight: 22, minHeight: 22,
}, },
titleIcon: {
width: 16,
height: 16,
marginRight: 6,
resizeMode: 'contain',
},
title: { title: {
fontSize: 14, fontSize: 14,
color: '#192126', color: '#192126',
fontWeight: '500', fontWeight: '600',
}, },
addButton: { addButton: {
borderRadius: 16, borderRadius: 16,

View File

@@ -1,3 +1,4 @@
import { Image } from 'expo-image';
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
@@ -22,7 +23,17 @@ const HealthDataCard: React.FC<HealthDataCardProps> = ({
style={[styles.card, style]} style={[styles.card, style]}
> >
<View style={styles.content}> <View style={styles.content}>
<Text style={styles.title}>{title}</Text> <View style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: 14,
}}>
<Image
source={require('@/assets/images/icons/icon-blood-oxygen.png')}
style={styles.titleIcon}
/>
<Text style={styles.title}>{title}</Text>
</View>
<View style={styles.valueContainer}> <View style={styles.valueContainer}>
<Text style={styles.value}>{value}</Text> <Text style={styles.value}>{value}</Text>
<Text style={styles.unit}>{unit}</Text> <Text style={styles.unit}>{unit}</Text>
@@ -51,10 +62,16 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
}, },
titleIcon: {
width: 16,
height: 16,
marginRight: 6,
resizeMode: 'contain',
},
title: { title: {
fontSize: 14, fontSize: 14,
color: '#192126', color: '#192126',
marginBottom: 14, fontWeight: '600',
}, },
valueContainer: { valueContainer: {
flexDirection: 'row', flexDirection: 'row',

View File

@@ -1,4 +1,5 @@
import { fetchCompleteSleepData, formatSleepTime } from '@/utils/sleepHealthKit'; import { fetchCompleteSleepData, formatSleepTime } from '@/utils/sleepHealthKit';
import { Image } from 'expo-image';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
@@ -39,6 +40,10 @@ const SleepCard: React.FC<SleepCardProps> = ({
const CardContent = ( const CardContent = (
<View style={[styles.container, style]}> <View style={[styles.container, style]}>
<View style={styles.cardHeaderRow}> <View style={styles.cardHeaderRow}>
<Image
source={require('@/assets/images/icons/icon-sleep.png')}
style={styles.titleIcon}
/>
<Text style={styles.cardTitle}></Text> <Text style={styles.cardTitle}></Text>
</View> </View>
<Text style={styles.sleepValue}> <Text style={styles.sleepValue}>
@@ -65,11 +70,18 @@ const styles = StyleSheet.create({
cardHeaderRow: { cardHeaderRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'flex-start',
},
titleIcon: {
width: 16,
height: 16,
marginRight: 6,
resizeMode: 'contain',
}, },
cardTitle: { cardTitle: {
fontSize: 14, fontSize: 14,
color: '#192126', color: '#192126',
fontWeight: '600',
}, },
sleepValue: { sleepValue: {
fontSize: 16, fontSize: 16,

View File

@@ -6,6 +6,7 @@ import { useColorScheme } from '@/hooks/useColorScheme';
import { fetchWeightHistory } from '@/store/userSlice'; import { fetchWeightHistory } from '@/store/userSlice';
import { BMI_CATEGORIES } from '@/utils/bmi'; import { BMI_CATEGORIES } from '@/utils/bmi';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
@@ -72,6 +73,10 @@ export function WeightHistoryCard() {
return ( return (
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}> <TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<Image
source={require('@/assets/images/icons/icon-weight.png')}
style={styles.iconSquare}
/>
<Text style={styles.cardTitle}></Text> <Text style={styles.cardTitle}></Text>
</View> </View>
@@ -156,6 +161,10 @@ export function WeightHistoryCard() {
return ( return (
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}> <TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<Image
source={require('@/assets/images/icons/icon-weight.png')}
style={styles.iconSquare}
/>
<Text style={styles.cardTitle}></Text> <Text style={styles.cardTitle}></Text>
<TouchableOpacity <TouchableOpacity
style={styles.addButton} style={styles.addButton}
@@ -345,17 +354,18 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
}, },
iconSquare: { iconSquare: {
width: 30, width: 14,
height: 30, height: 14,
borderRadius: 8, borderRadius: 8,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 2, marginRight: 4,
}, },
cardTitle: { cardTitle: {
fontSize: 14, fontSize: 14,
color: '#192126', color: '#192126',
flex: 1, flex: 1,
fontWeight: '600'
}, },
headerButtons: { headerButtons: {
flexDirection: 'row', flexDirection: 'row',

View File

@@ -21,8 +21,6 @@ export function useAuthGuard() {
const currentPath = usePathname(); const currentPath = usePathname();
const token = useAppSelector((s) => (s as any)?.user?.token as string | null); const token = useAppSelector((s) => (s as any)?.user?.token as string | null);
console.log('useAuthGuard!!!token', token);
const isLoggedIn = !!token; const isLoggedIn = !!token;
const ensureLoggedIn = useCallback(async (options?: EnsureOptions): Promise<boolean> => { const ensureLoggedIn = useCallback(async (options?: EnsureOptions): Promise<boolean> => {

View File

@@ -179,8 +179,8 @@ PODS:
- ReactCommon/turbomodule/core - ReactCommon/turbomodule/core
- SocketRocket - SocketRocket
- Yoga - Yoga
- PurchasesHybridCommon (16.2.2): - PurchasesHybridCommon (17.7.0):
- RevenueCat (= 5.34.0) - RevenueCat (= 5.39.0)
- RCT-Folly (2024.11.18.00): - RCT-Folly (2024.11.18.00):
- boost - boost
- DoubleConversion - DoubleConversion
@@ -2545,7 +2545,7 @@ PODS:
- React-perflogger (= 0.81.4) - React-perflogger (= 0.81.4)
- React-utils (= 0.81.4) - React-utils (= 0.81.4)
- SocketRocket - SocketRocket
- RevenueCat (5.34.0) - RevenueCat (5.39.0)
- RNAppleHealthKit (1.7.0): - RNAppleHealthKit (1.7.0):
- React - React
- RNCAsyncStorage (2.2.0): - RNCAsyncStorage (2.2.0):
@@ -2690,8 +2690,8 @@ PODS:
- ReactCommon/turbomodule/core - ReactCommon/turbomodule/core
- SocketRocket - SocketRocket
- Yoga - Yoga
- RNPurchases (9.2.2): - RNPurchases (9.4.3):
- PurchasesHybridCommon (= 16.2.2) - PurchasesHybridCommon (= 17.7.0)
- React-Core - React-Core
- RNReanimated (4.1.0): - RNReanimated (4.1.0):
- boost - boost
@@ -3463,7 +3463,7 @@ SPEC CHECKSUMS:
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
lottie-ios: a881093fab623c467d3bce374367755c272bdd59 lottie-ios: a881093fab623c467d3bce374367755c272bdd59
lottie-react-native: 86fa7488b1476563f41071244aa73d5b738faf19 lottie-react-native: 86fa7488b1476563f41071244aa73d5b738faf19
PurchasesHybridCommon: 62f852419aae7041792217593998f7ac3f8b567d PurchasesHybridCommon: 6bc96162fb0c061e1980f474be618c088cfd1428
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c0ed3249a97243002615517dff789bf4666cf585 RCTDeprecation: c0ed3249a97243002615517dff789bf4666cf585
RCTRequired: 58719f5124f9267b5f9649c08bf23d9aea845b23 RCTRequired: 58719f5124f9267b5f9649c08bf23d9aea845b23
@@ -3531,7 +3531,7 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: 433ddfb4536948630aadd5bd925aff8a632d2fe3 ReactAppDependencyProvider: 433ddfb4536948630aadd5bd925aff8a632d2fe3
ReactCodegen: 1d05923ad119796be9db37830d5e5dc76586aa00 ReactCodegen: 1d05923ad119796be9db37830d5e5dc76586aa00
ReactCommon: 394c6b92765cf6d211c2c3f7f6bc601dffb316a6 ReactCommon: 394c6b92765cf6d211c2c3f7f6bc601dffb316a6
RevenueCat: eb2aa042789d9c99ad5172bd96e28b96286d6ada RevenueCat: 4743a5eee0004e1c03eabeb3498818f902a5d622
RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9 RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9
RNCAsyncStorage: 29f0230e1a25f36c20b05f65e2eb8958d6526e82 RNCAsyncStorage: 29f0230e1a25f36c20b05f65e2eb8958d6526e82
RNCMaskedView: 5ef8c95cbab95334a32763b72896a7b7d07e6299 RNCMaskedView: 5ef8c95cbab95334a32763b72896a7b7d07e6299
@@ -3539,7 +3539,7 @@ SPEC CHECKSUMS:
RNDateTimePicker: cda4c045beca864cebb3209ef9cc4094f974864c RNDateTimePicker: cda4c045beca864cebb3209ef9cc4094f974864c
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047 RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961 RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961
RNPurchases: 7993b33416e67d5863140b5c62c682b34719f475 RNPurchases: 1bc60e3a69af65d9cfe23967328421dd1df1763c
RNReanimated: 9de34f0313c4177a34c079ca9fce6f1f278bff24 RNReanimated: 9de34f0313c4177a34c079ca9fce6f1f278bff24
RNScreens: 0bbf16c074ae6bb1058a7bf2d1ae017f4306797c RNScreens: 0bbf16c074ae6bb1058a7bf2d1ae017f4306797c
RNSentry: f2c39f1113e22413c9bb6e3faa6b27f110d95eaf RNSentry: f2c39f1113e22413c9bb6e3faa6b27f110d95eaf

32
package-lock.json generated
View File

@@ -58,7 +58,7 @@
"react-native-markdown-display": "^7.0.2", "react-native-markdown-display": "^7.0.2",
"react-native-modal-datetime-picker": "^18.0.0", "react-native-modal-datetime-picker": "^18.0.0",
"react-native-popover-view": "^6.1.0", "react-native-popover-view": "^6.1.0",
"react-native-purchases": "^9.2.2", "react-native-purchases": "^9.4.3",
"react-native-reanimated": "~4.1.0", "react-native-reanimated": "~4.1.0",
"react-native-render-html": "^6.3.4", "react-native-render-html": "^6.3.4",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
@@ -3967,24 +3967,24 @@
} }
}, },
"node_modules/@revenuecat/purchases-js": { "node_modules/@revenuecat/purchases-js": {
"version": "1.11.1", "version": "1.14.0",
"resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-js/-/purchases-js-1.11.1.tgz", "resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-js/-/purchases-js-1.14.0.tgz",
"integrity": "sha512-P0jxwUBWOIFSZQ1/NIMpbOXG3brraNDGYoCnES1r5w97yonhAw1brpKwhFKUhlq+DvAUDCG1q1d8FdTzI+MgXg==", "integrity": "sha512-SotJ9MznBZoq91nSxLl1Oqw42k6A3JU0d73/WFbppv1IG770Q8KhuAUo1SgZC62ERCuCHme5fEdER1ZI4x8y7A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@revenuecat/purchases-js-hybrid-mappings": { "node_modules/@revenuecat/purchases-js-hybrid-mappings": {
"version": "16.2.1", "version": "17.7.0",
"resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-js-hybrid-mappings/-/purchases-js-hybrid-mappings-16.2.1.tgz", "resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-js-hybrid-mappings/-/purchases-js-hybrid-mappings-17.7.0.tgz",
"integrity": "sha512-TXYw6lh5rg/kGI44kayU4TGSXKDcc35TdB0vBuZfllSokY1tnyYmP8Pm2eZamLN8ycrTuCysoPxknW2Klh1H1g==", "integrity": "sha512-254jmgsCxn7Om49gdRQmh8rNVPsEjpMcKmrIwiR8D4Yfcch4bWUK23bDNvFR29Ygen9ctBkGc+CklmmKNQMQYw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@revenuecat/purchases-js": "1.11.1" "@revenuecat/purchases-js": "1.14.0"
} }
}, },
"node_modules/@revenuecat/purchases-typescript-internal": { "node_modules/@revenuecat/purchases-typescript-internal": {
"version": "16.2.1", "version": "17.7.0",
"resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-typescript-internal/-/purchases-typescript-internal-16.2.1.tgz", "resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-typescript-internal/-/purchases-typescript-internal-17.7.0.tgz",
"integrity": "sha512-g7FhNA6nxr9686klimlfueMQqQl34pHUHXeCKXqeuaPJOOsFc7qcOGhGZdyLGulIAgpkctrvcAbeDyBk7t5QRg==", "integrity": "sha512-CGyNcupvNEnyiZTRsD98VOLRxsp6cr1NYIL8XKDO2EoSDBaDuDXyHTADbPEf0lRW2qYqwmQKZFJ61Vwl+0YiWw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
@@ -12673,17 +12673,17 @@
} }
}, },
"node_modules/react-native-purchases": { "node_modules/react-native-purchases": {
"version": "9.2.2", "version": "9.4.3",
"resolved": "https://mirrors.tencent.com/npm/react-native-purchases/-/react-native-purchases-9.2.2.tgz", "resolved": "https://mirrors.tencent.com/npm/react-native-purchases/-/react-native-purchases-9.4.3.tgz",
"integrity": "sha512-j376mva8G6SLA2HPTROpUGoivfLMZVWPM7mj2bcgTS8y6NzbyQJ20Npe8V3nWc0N5YFTuknTF8pl0tWc6FqYbA==", "integrity": "sha512-7M8/tjZY2iZISBOqXIxxzkt7fxt86WGF2ZW/qcsEfXVaZv2UvDLs+GuYKjf0Bz+2hOuYwoJK/LF++jpUi6CRng==",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"examples/purchaseTesterTypescript", "examples/purchaseTesterTypescript",
"react-native-purchases-ui" "react-native-purchases-ui"
], ],
"dependencies": { "dependencies": {
"@revenuecat/purchases-js-hybrid-mappings": "16.2.1", "@revenuecat/purchases-js-hybrid-mappings": "17.7.0",
"@revenuecat/purchases-typescript-internal": "16.2.1" "@revenuecat/purchases-typescript-internal": "17.7.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">= 16.6.3", "react": ">= 16.6.3",

View File

@@ -62,7 +62,7 @@
"react-native-markdown-display": "^7.0.2", "react-native-markdown-display": "^7.0.2",
"react-native-modal-datetime-picker": "^18.0.0", "react-native-modal-datetime-picker": "^18.0.0",
"react-native-popover-view": "^6.1.0", "react-native-popover-view": "^6.1.0",
"react-native-purchases": "^9.2.2", "react-native-purchases": "^9.4.3",
"react-native-reanimated": "~4.1.0", "react-native-reanimated": "~4.1.0",
"react-native-render-html": "^6.3.4", "react-native-render-html": "^6.3.4",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",

View File

@@ -92,7 +92,7 @@ export type UserState = {
activityHistory: ActivityHistoryItem[]; activityHistory: ActivityHistoryItem[];
}; };
export const DEFAULT_MEMBER_NAME = '小海豹'; export const DEFAULT_MEMBER_NAME = '';
const getInitialState = (): UserState => { const getInitialState = (): UserState => {
const preloaded = getPreloadedUserData(); const preloaded = getPreloadedUserData();