Add comprehensive app update checking functionality with: - New VersionCheckContext for managing update detection and notifications - VersionUpdateModal UI component for presenting update information - Version service API integration with platform-specific update URLs - Version check menu item in personal settings with manual/automatic checking Enhance internationalization across workout features: - Complete workout type translations for English and Chinese - Localized workout detail modal with proper date/time formatting - Locale-aware date formatting in fitness rings detail - Workout notification improvements with deep linking to specific workout details Improve UI/UX with better chart rendering, sizing fixes, and enhanced navigation flow. Update app version to 1.1.3 and include app version in API headers for better tracking.
504 lines
14 KiB
TypeScript
504 lines
14 KiB
TypeScript
import { Colors } from '@/constants/Colors';
|
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
import { useWaterDataByDate } from '@/hooks/useWaterData';
|
|
import { getQuickWaterAmount } from '@/utils/userPreferences';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
|
import { Image } from 'expo-image';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { router, useLocalSearchParams } from 'expo-router';
|
|
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Alert,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View
|
|
} from 'react-native';
|
|
import { Swipeable } from 'react-native-gesture-handler';
|
|
|
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
|
import { useI18n } from '@/hooks/useI18n';
|
|
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
|
import dayjs from 'dayjs';
|
|
|
|
interface WaterDetailProps {
|
|
selectedDate?: string;
|
|
}
|
|
|
|
const WaterDetail: React.FC<WaterDetailProps> = () => {
|
|
const { t } = useI18n();
|
|
const safeAreaTop = useSafeAreaTop()
|
|
|
|
const { selectedDate } = useLocalSearchParams<{ selectedDate?: string }>();
|
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
|
const colorTokens = Colors[theme];
|
|
|
|
const [dailyGoal, setDailyGoal] = useState<string>('2000');
|
|
const [quickAddAmount, setQuickAddAmount] = useState<string>('250');
|
|
|
|
// 使用新的 hook 来处理指定日期的饮水数据
|
|
const { waterRecords, dailyWaterGoal, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate);
|
|
|
|
// 处理设置按钮点击 - 跳转到设置页面
|
|
const handleSettingsPress = () => {
|
|
router.push('/water/settings');
|
|
};
|
|
|
|
// 删除饮水记录
|
|
const handleDeleteRecord = async (recordId: string) => {
|
|
await removeWaterRecord(recordId);
|
|
};
|
|
|
|
// 加载用户偏好设置和当前饮水目标
|
|
useEffect(() => {
|
|
const loadUserPreferences = async () => {
|
|
try {
|
|
const amount = await getQuickWaterAmount();
|
|
setQuickAddAmount(amount.toString());
|
|
|
|
// 设置当前的饮水目标
|
|
if (dailyWaterGoal) {
|
|
setDailyGoal(dailyWaterGoal.toString());
|
|
}
|
|
} catch (error) {
|
|
console.error(t('waterDetail.loadingUserPreferences'), error);
|
|
}
|
|
};
|
|
|
|
loadUserPreferences();
|
|
}, [dailyWaterGoal]);
|
|
|
|
const totalAmount = waterRecords?.reduce((sum, record) => sum + record.amount, 0) || 0;
|
|
const currentGoal = dailyWaterGoal || 2000;
|
|
const progress = Math.min(100, (totalAmount / currentGoal) * 100);
|
|
|
|
// 新增:饮水记录卡片组件
|
|
const WaterRecordCard = ({ record, onDelete }: { record: any; onDelete: () => void }) => {
|
|
const swipeableRef = React.useRef<Swipeable>(null);
|
|
|
|
// 处理删除操作
|
|
const handleDelete = () => {
|
|
Alert.alert(
|
|
t('waterDetail.deleteConfirm.title'),
|
|
t('waterDetail.deleteConfirm.message'),
|
|
[
|
|
{
|
|
text: t('waterDetail.deleteConfirm.cancel'),
|
|
style: 'cancel',
|
|
},
|
|
{
|
|
text: t('waterDetail.deleteConfirm.confirm'),
|
|
style: 'destructive',
|
|
onPress: () => {
|
|
onDelete();
|
|
swipeableRef.current?.close();
|
|
},
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
// 渲染右侧删除按钮
|
|
const renderRightActions = () => {
|
|
return (
|
|
<TouchableOpacity
|
|
style={styles.deleteSwipeButton}
|
|
onPress={handleDelete}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Ionicons name="trash" size={20} color="#FFFFFF" />
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<View style={styles.recordCardContainer}>
|
|
<Swipeable
|
|
ref={swipeableRef}
|
|
renderRightActions={renderRightActions}
|
|
rightThreshold={40}
|
|
overshootRight={false}
|
|
>
|
|
<View style={styles.recordCard}>
|
|
<View style={styles.recordMainContent}>
|
|
<View style={styles.recordIconContainer}>
|
|
<Image
|
|
source={require('@/assets/images/icons/IconGlass.png')}
|
|
style={styles.recordIcon}
|
|
/>
|
|
</View>
|
|
<View style={styles.recordInfo}>
|
|
<Text style={styles.recordLabel}>{t('waterDetail.water')}</Text>
|
|
<View style={styles.recordTimeContainer}>
|
|
<Ionicons name="time-outline" size={14} color="#6f7ba7" />
|
|
<Text style={styles.recordTimeText}>
|
|
{dayjs(record.recordedAt || record.createdAt).format('HH:mm')}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.recordAmountContainer}>
|
|
<Text style={styles.recordAmount}>{record.amount}ml</Text>
|
|
</View>
|
|
</View>
|
|
{record.note && (
|
|
<Text style={styles.recordNote}>{record.note}</Text>
|
|
)}
|
|
</View>
|
|
</Swipeable>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
{/* 背景 */}
|
|
<LinearGradient
|
|
colors={['#f3f4fb', '#f3f4fb']}
|
|
style={StyleSheet.absoluteFillObject}
|
|
/>
|
|
{/* 顶部装饰性渐变 - 模仿挑战页面的柔和背景感 */}
|
|
<LinearGradient
|
|
colors={['rgba(229, 252, 254, 0.8)', 'rgba(243, 244, 251, 0)']}
|
|
style={styles.topGradient}
|
|
start={{ x: 0.5, y: 0 }}
|
|
end={{ x: 0.5, y: 1 }}
|
|
/>
|
|
|
|
<HeaderBar
|
|
title={t('waterDetail.title')}
|
|
onBack={() => router.back()}
|
|
right={
|
|
isLiquidGlassAvailable() ? (
|
|
<TouchableOpacity
|
|
onPress={handleSettingsPress}
|
|
activeOpacity={0.7}
|
|
style={styles.settingsButtonWrapper}
|
|
>
|
|
<GlassView
|
|
style={styles.settingsButtonGlass}
|
|
glassEffectStyle="regular"
|
|
tintColor="rgba(255, 255, 255, 0.4)"
|
|
isInteractive={true}
|
|
>
|
|
<Ionicons name="settings-outline" size={22} color="#1c1f3a" />
|
|
</GlassView>
|
|
</TouchableOpacity>
|
|
) : (
|
|
<TouchableOpacity
|
|
style={styles.settingsButtonFallback}
|
|
onPress={handleSettingsPress}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Ionicons name="settings-outline" size={22} color="#1c1f3a" />
|
|
</TouchableOpacity>
|
|
)
|
|
}
|
|
/>
|
|
|
|
<KeyboardAvoidingView
|
|
style={styles.keyboardAvoidingView}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
>
|
|
<ScrollView
|
|
style={styles.scrollView}
|
|
contentContainerStyle={[styles.scrollContent, {
|
|
paddingTop: safeAreaTop
|
|
}]}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<View style={styles.headerBlock}>
|
|
<Text style={styles.pageTitle}>
|
|
{selectedDate ? dayjs(selectedDate).format('MM-DD') : t('waterDetail.today')}
|
|
</Text>
|
|
<Text style={styles.pageSubtitle}>{t('waterDetail.waterRecord')}</Text>
|
|
</View>
|
|
|
|
{/* 进度卡片 */}
|
|
<View style={styles.progressCard}>
|
|
<View style={styles.progressInfo}>
|
|
<View>
|
|
<Text style={styles.progressLabel}>{t('waterDetail.total')}</Text>
|
|
<Text style={styles.progressValue}>{totalAmount}<Text style={styles.progressUnit}>ml</Text></Text>
|
|
</View>
|
|
<View style={{ alignItems: 'flex-end' }}>
|
|
<Text style={styles.progressLabel}>{t('waterDetail.goal')}</Text>
|
|
<Text style={styles.progressGoalValue}>{currentGoal}<Text style={styles.progressUnit}>ml</Text></Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.progressBarBg}>
|
|
<LinearGradient
|
|
colors={['#4F5BD5', '#6B6CFF']}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
style={[styles.progressBarFill, { width: `${progress}%` }]}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 记录列表 */}
|
|
<View style={styles.section}>
|
|
{waterRecords && waterRecords.length > 0 ? (
|
|
<View style={styles.recordsList}>
|
|
{waterRecords.map((record) => (
|
|
<WaterRecordCard
|
|
key={record.id}
|
|
record={record}
|
|
onDelete={() => handleDeleteRecord(record.id)}
|
|
/>
|
|
))}
|
|
</View>
|
|
) : (
|
|
<View style={styles.noRecordsContainer}>
|
|
<Image
|
|
source={require('@/assets/images/icons/IconGlass.png')}
|
|
style={{ width: 60, height: 60, opacity: 0.5, marginBottom: 16 }}
|
|
/>
|
|
<Text style={styles.noRecordsText}>{t('waterDetail.noRecords')}</Text>
|
|
<Text style={styles.noRecordsSubText}>{t('waterDetail.noRecordsSubtitle')}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#f3f4fb',
|
|
},
|
|
topGradient: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
height: 300,
|
|
},
|
|
keyboardAvoidingView: {
|
|
flex: 1,
|
|
},
|
|
scrollView: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
paddingBottom: 40,
|
|
},
|
|
headerBlock: {
|
|
paddingHorizontal: 24,
|
|
marginTop: 10,
|
|
marginBottom: 24,
|
|
},
|
|
pageTitle: {
|
|
fontSize: 28,
|
|
fontWeight: '800',
|
|
color: '#1c1f3a',
|
|
fontFamily: 'AliBold',
|
|
marginBottom: 4,
|
|
},
|
|
pageSubtitle: {
|
|
fontSize: 16,
|
|
color: '#6f7ba7',
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
|
|
// 进度卡片
|
|
progressCard: {
|
|
marginHorizontal: 24,
|
|
marginBottom: 32,
|
|
padding: 24,
|
|
borderRadius: 28,
|
|
backgroundColor: '#ffffff',
|
|
shadowColor: 'rgba(30, 41, 59, 0.1)',
|
|
shadowOffset: { width: 0, height: 10 },
|
|
shadowOpacity: 0.18,
|
|
shadowRadius: 20,
|
|
elevation: 8,
|
|
},
|
|
progressInfo: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-end',
|
|
marginBottom: 16,
|
|
},
|
|
progressLabel: {
|
|
fontSize: 14,
|
|
color: '#6f7ba7',
|
|
marginBottom: 6,
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
progressValue: {
|
|
fontSize: 28,
|
|
fontWeight: '800',
|
|
color: '#4F5BD5',
|
|
fontFamily: 'AliBold',
|
|
lineHeight: 32,
|
|
},
|
|
progressGoalValue: {
|
|
fontSize: 20,
|
|
fontWeight: '700',
|
|
color: '#1c1f3a',
|
|
fontFamily: 'AliBold',
|
|
lineHeight: 32,
|
|
},
|
|
progressUnit: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
color: '#6f7ba7',
|
|
marginLeft: 2,
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
progressBarBg: {
|
|
height: 12,
|
|
backgroundColor: '#F0F2F5',
|
|
borderRadius: 6,
|
|
overflow: 'hidden',
|
|
},
|
|
progressBarFill: {
|
|
height: '100%',
|
|
borderRadius: 6,
|
|
},
|
|
|
|
section: {
|
|
paddingHorizontal: 24,
|
|
},
|
|
|
|
// 记录列表样式
|
|
recordsList: {
|
|
gap: 16,
|
|
},
|
|
recordCardContainer: {
|
|
shadowColor: 'rgba(30, 41, 59, 0.08)',
|
|
shadowOffset: { width: 0, height: 8 },
|
|
shadowOpacity: 0.12,
|
|
shadowRadius: 12,
|
|
elevation: 4,
|
|
marginBottom: 2,
|
|
},
|
|
recordCard: {
|
|
borderRadius: 24,
|
|
padding: 18,
|
|
backgroundColor: '#ffffff',
|
|
},
|
|
recordMainContent: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
recordIconContainer: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 16,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: '#f5f6ff',
|
|
},
|
|
recordIcon: {
|
|
width: 24,
|
|
height: 24,
|
|
},
|
|
recordInfo: {
|
|
flex: 1,
|
|
marginLeft: 16,
|
|
},
|
|
recordLabel: {
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
color: '#1c1f3a',
|
|
marginBottom: 4,
|
|
fontFamily: 'AliBold',
|
|
},
|
|
recordTimeContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
recordTimeText: {
|
|
fontSize: 13,
|
|
color: '#6f7ba7',
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
recordAmountContainer: {
|
|
alignItems: 'flex-end',
|
|
},
|
|
recordAmount: {
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
color: '#4F5BD5',
|
|
fontFamily: 'AliBold',
|
|
},
|
|
recordNote: {
|
|
marginTop: 14,
|
|
padding: 12,
|
|
backgroundColor: '#F8F9FC',
|
|
borderRadius: 12,
|
|
fontSize: 13,
|
|
lineHeight: 18,
|
|
color: '#5f6a97',
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
deleteSwipeButton: {
|
|
backgroundColor: '#FF6B6B',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
width: 70,
|
|
height: '100%',
|
|
borderRadius: 24,
|
|
marginLeft: 12,
|
|
},
|
|
|
|
noRecordsContainer: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingVertical: 60,
|
|
backgroundColor: '#ffffff',
|
|
borderRadius: 28,
|
|
shadowColor: 'rgba(30, 41, 59, 0.06)',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 12,
|
|
},
|
|
noRecordsText: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
color: '#1c1f3a',
|
|
marginBottom: 8,
|
|
fontFamily: 'AliBold',
|
|
},
|
|
noRecordsSubText: {
|
|
fontSize: 14,
|
|
color: '#9ba3c7',
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
|
|
// Settings Button
|
|
settingsButtonWrapper: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
overflow: 'hidden',
|
|
},
|
|
settingsButtonGlass: {
|
|
width: 40,
|
|
height: 40,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
settingsButtonFallback: {
|
|
width: 40,
|
|
height: 40,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderRadius: 20,
|
|
backgroundColor: '#ffffff',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(0,0,0,0.05)',
|
|
},
|
|
});
|
|
|
|
export default WaterDetail;
|