feat(ui): 统一健康卡片标题图标并优化语音录音稳定性
- 为所有健康数据卡片添加对应功能图标,提升视觉一致性 - 将“小鱼干”文案统一为“能量值”,并更新获取说明 - 语音录音页面增加组件卸载保护、错误提示与资源清理逻辑 - 个人页支持毛玻璃按钮样式,默认用户名置空 - 新增血氧、饮食、心情、压力、睡眠、步数、体重等图标资源 - 升级 react-native-purchases 至 9.4.3 - 移除 useAuthGuard 调试日志
@@ -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) {
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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] || '';
|
||||||
|
if (text.trim()) {
|
||||||
setRecognizedText(text);
|
setRecognizedText(text);
|
||||||
setRecordState('result');
|
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,37 +196,70 @@ 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 () => {
|
||||||
|
try {
|
||||||
// 停止所有动画
|
// 停止所有动画
|
||||||
stopAnimations();
|
stopAnimations();
|
||||||
|
|
||||||
@@ -205,16 +270,17 @@ export default function VoiceRecordScreen() {
|
|||||||
setRecordState('idle');
|
setRecordState('idle');
|
||||||
|
|
||||||
// 确保语音识别已停止
|
// 确保语音识别已停止
|
||||||
try {
|
|
||||||
await Voice.stop();
|
await Voice.stop();
|
||||||
} catch {
|
|
||||||
// 忽略停止错误,可能已经停止了
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延迟一点再开始新的录音,确保状态完全重置
|
// 延迟一点再开始新的录音,确保状态完全重置
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
startRecording();
|
startRecording();
|
||||||
}, 100);
|
}, 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 () => {
|
||||||
|
try {
|
||||||
|
// 如果正在录音,先停止
|
||||||
if (isListening) {
|
if (isListening) {
|
||||||
stopRecording();
|
await stopRecording();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 停止所有动画
|
||||||
|
stopAnimations();
|
||||||
|
|
||||||
|
// 确保语音识别完全停止
|
||||||
|
await Voice.stop();
|
||||||
|
|
||||||
router.back();
|
router.back();
|
||||||
|
} catch (error) {
|
||||||
|
console.log('返回时清理资源失败:', error);
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取状态对应的UI文本
|
// 获取状态对应的UI文本
|
||||||
|
|||||||
BIN
assets/images/icons/icon-blood-oxygen.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 33 KiB |
BIN
assets/images/icons/icon-healthy-diet.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
assets/images/icons/icon-mood.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/images/icons/icon-pressure.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/images/icons/icon-sleep.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/images/icons/icon-step.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/images/icons/icon-weight.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
<View style={styles.titleContainer}>
|
||||||
|
<Image
|
||||||
|
source={require('@/assets/images/icons/icon-mood.png')}
|
||||||
|
style={styles.titleIcon}
|
||||||
|
/>
|
||||||
<Text style={styles.cardTitle}>心情</Text>
|
<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: {
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
<View style={styles.titleContainer}>
|
||||||
|
<Image
|
||||||
|
source={require('@/assets/images/icons/icon-healthy-diet.png')}
|
||||||
|
style={styles.titleIcon}
|
||||||
|
/>
|
||||||
<Text style={styles.cardTitle}>饮食分析</Text>
|
<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,
|
||||||
|
|||||||
@@ -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%',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Image
|
||||||
|
source={require('@/assets/images/icons/IconGlass.png')}
|
||||||
|
style={styles.titleIcon}
|
||||||
|
/>
|
||||||
<Text style={styles.title}>喝水</Text>
|
<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,
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
<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>
|
<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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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> => {
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||