feat: 移除目标管理演示页面并优化相关组件
- 删除目标管理演示页面的代码,简化项目结构 - 更新底部导航,移除目标管理演示页面的路由 - 调整相关组件的样式和逻辑,确保界面一致性 - 优化颜色常量的使用,提升视觉效果
This commit is contained in:
@@ -57,7 +57,7 @@ export default function TabLayout() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { icon, title } = getIconAndTitle();
|
const { icon, title } = getIconAndTitle();
|
||||||
const activeContentColor = colorTokens.onPrimary;
|
const activeContentColor = colorTokens.tabIconSelected; // 使用专门为Tab定义的选中颜色
|
||||||
const inactiveContentColor = colorTokens.tabIconDefault;
|
const inactiveContentColor = colorTokens.tabIconDefault;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||||
import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach';
|
import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach';
|
||||||
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
|
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
|
||||||
@@ -124,7 +125,8 @@ export default function CoachScreen() {
|
|||||||
|
|
||||||
const { isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard();
|
const { isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard();
|
||||||
// 为了让页面更贴近品牌主题与更亮的观感,这里使用亮色系配色
|
// 为了让页面更贴近品牌主题与更亮的观感,这里使用亮色系配色
|
||||||
const theme = Colors.light;
|
const colorScheme = useColorScheme();
|
||||||
|
const theme = Colors[colorScheme ?? 'light'];
|
||||||
const botName = (params?.name || 'Seal').toString();
|
const botName = (params?.name || 'Seal').toString();
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
@@ -282,6 +284,16 @@ export default function CoachScreen() {
|
|||||||
{ key: 'weight', label: '#记体重', action: () => insertWeightInputCard() },
|
{ key: 'weight', label: '#记体重', action: () => insertWeightInputCard() },
|
||||||
{ key: 'diet', label: '#记饮食', action: () => insertDietInputCard() },
|
{ key: 'diet', label: '#记饮食', action: () => insertDietInputCard() },
|
||||||
{ key: 'dietPlan', label: '#饮食方案', action: () => insertDietPlanCard() },
|
{ key: 'dietPlan', label: '#饮食方案', action: () => insertDietPlanCard() },
|
||||||
|
{
|
||||||
|
key: 'mood',
|
||||||
|
label: '#记心情',
|
||||||
|
action: () => {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
}
|
||||||
|
router.push('/mood/calendar');
|
||||||
|
}
|
||||||
|
},
|
||||||
], [router, planDraft, checkin]);
|
], [router, planDraft, checkin]);
|
||||||
|
|
||||||
const scrollToEnd = useCallback(() => {
|
const scrollToEnd = useCallback(() => {
|
||||||
@@ -1333,7 +1345,7 @@ export default function CoachScreen() {
|
|||||||
{/* 标题部分 */}
|
{/* 标题部分 */}
|
||||||
<View style={styles.dietPlanHeader}>
|
<View style={styles.dietPlanHeader}>
|
||||||
<View style={styles.dietPlanTitleContainer}>
|
<View style={styles.dietPlanTitleContainer}>
|
||||||
<Ionicons name="restaurant-outline" size={20} color={Colors.light.accentGreenDark} />
|
<Ionicons name="restaurant-outline" size={20} color={theme.success} />
|
||||||
<Text style={styles.dietPlanTitle}>我的饮食方案</Text>
|
<Text style={styles.dietPlanTitle}>我的饮食方案</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.dietPlanSubtitle}>MY DIET PLAN</Text>
|
<Text style={styles.dietPlanSubtitle}>MY DIET PLAN</Text>
|
||||||
@@ -1522,7 +1534,7 @@ export default function CoachScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{isSelected && isPending && (
|
{isSelected && isPending && (
|
||||||
<ActivityIndicator size="small" color={Colors.light.accentGreenDark} />
|
<ActivityIndicator size="small" color={theme.success} />
|
||||||
)}
|
)}
|
||||||
{isSelected && !isPending && (
|
{isSelected && !isPending && (
|
||||||
<View style={styles.selectedBadge}>
|
<View style={styles.selectedBadge}>
|
||||||
@@ -1829,7 +1841,7 @@ export default function CoachScreen() {
|
|||||||
<View style={styles.screen}>
|
<View style={styles.screen}>
|
||||||
{/* 背景渐变 */}
|
{/* 背景渐变 */}
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['#F0F9FF', '#E0F2FE']}
|
colors={['#fafaff', '#f4f3ff']} // 使用紫色主题的浅色渐变
|
||||||
style={styles.gradientBackground}
|
style={styles.gradientBackground}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
@@ -1866,7 +1878,7 @@ export default function CoachScreen() {
|
|||||||
source={require('@/assets/images/icons/iconFlash.png')}
|
source={require('@/assets/images/icons/iconFlash.png')}
|
||||||
style={styles.usageIcon}
|
style={styles.usageIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.usageText, { color: theme.text }]}>
|
<Text style={styles.usageText}>
|
||||||
{userProfile?.isVip ? '不限' : `${userProfile?.freeUsageCount || 0}/${userProfile?.maxUsageCount || 0}`}
|
{userProfile?.isVip ? '不限' : `${userProfile?.freeUsageCount || 0}/${userProfile?.maxUsageCount || 0}`}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -1876,16 +1888,16 @@ export default function CoachScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={startNewConversation}
|
onPress={startNewConversation}
|
||||||
style={[styles.headerActionButton, { backgroundColor: `${Colors.light.accentGreen}33` }]} // 20% opacity
|
style={[styles.headerActionButton, { backgroundColor: `${theme.primary}20` }]} // 20% opacity
|
||||||
>
|
>
|
||||||
<Ionicons name="add-outline" size={18} color={theme.onPrimary} />
|
<Ionicons name="add-outline" size={18} color={theme.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={openHistory}
|
onPress={openHistory}
|
||||||
style={[styles.headerActionButton, { backgroundColor: `${Colors.light.accentGreen}33` }]} // 20% opacity
|
style={[styles.headerActionButton, { backgroundColor: `${theme.primary}20` }]} // 20% opacity
|
||||||
>
|
>
|
||||||
<Ionicons name="time-outline" size={18} color={theme.onPrimary} />
|
<Ionicons name="time-outline" size={18} color={theme.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -1948,8 +1960,18 @@ export default function CoachScreen() {
|
|||||||
contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }}
|
contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }}
|
||||||
>
|
>
|
||||||
{chips.map((c) => (
|
{chips.map((c) => (
|
||||||
<TouchableOpacity key={c.key} style={[styles.chip, { borderColor: `${Colors.light.accentGreen}59`, backgroundColor: `${Colors.light.accentGreen}1F` }]} onPress={c.action}>
|
<TouchableOpacity
|
||||||
<Text style={[styles.chipText, { color: '#192126' }]}>{c.label}</Text>
|
key={c.key}
|
||||||
|
style={[
|
||||||
|
styles.chip,
|
||||||
|
{
|
||||||
|
borderColor: c.key === 'mood' ? `${theme.success}40` : `${theme.primary}40`,
|
||||||
|
backgroundColor: c.key === 'mood' ? `${theme.success}15` : `${theme.primary}15`
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
onPress={c.action}
|
||||||
|
>
|
||||||
|
<Text style={[styles.chipText, { color: c.key === 'mood' ? theme.success : theme.text }]}>{c.label}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -1990,18 +2012,18 @@ export default function CoachScreen() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={[styles.inputRow, { borderColor: `${Colors.light.accentGreen}59`, backgroundColor: `${Colors.light.accentGreen}14` }]}>
|
<View style={[styles.inputRow, { borderColor: `${theme.primary}30`, backgroundColor: `${theme.primary}08` }]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={pickImages}
|
onPress={pickImages}
|
||||||
style={[styles.mediaBtn, { backgroundColor: `${Colors.light.accentGreen}28` }]}
|
style={[styles.mediaBtn, { backgroundColor: `${theme.primary}20` }]}
|
||||||
>
|
>
|
||||||
<Ionicons name="image-outline" size={18} color={'#192126'} />
|
<Ionicons name="image-outline" size={18} color={theme.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="问我任何健康相关的问题,如营养、健身、生活管理等..."
|
placeholder="问我任何健康相关的问题,如营养、健身、生活管理等..."
|
||||||
placeholderTextColor={theme.textMuted}
|
placeholderTextColor={theme.textMuted}
|
||||||
style={[styles.input, { color: '#192126' }]}
|
style={[styles.input, { color: theme.text }]}
|
||||||
value={input}
|
value={input}
|
||||||
onChangeText={setInput}
|
onChangeText={setInput}
|
||||||
multiline
|
multiline
|
||||||
@@ -2021,7 +2043,7 @@ export default function CoachScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.sendBtn,
|
styles.sendBtn,
|
||||||
{
|
{
|
||||||
backgroundColor: (isSending || isStreaming) ? '#FF4444' : theme.primary,
|
backgroundColor: (isSending || isStreaming) ? theme.danger : theme.primary,
|
||||||
opacity: ((input.trim() || selectedImages.length > 0) || (isSending || isStreaming)) ? 1 : 0.5
|
opacity: ((input.trim() || selectedImages.length > 0) || (isSending || isStreaming)) ? 1 : 0.5
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
@@ -2144,8 +2166,8 @@ const styles = StyleSheet.create({
|
|||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
borderRadius: 30,
|
borderRadius: 30,
|
||||||
backgroundColor: '#0EA5E9',
|
backgroundColor: '#7a5af8', // 紫色主题
|
||||||
opacity: 0.1,
|
opacity: 0.08,
|
||||||
},
|
},
|
||||||
decorativeCircle2: {
|
decorativeCircle2: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -2154,8 +2176,8 @@ const styles = StyleSheet.create({
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
backgroundColor: '#0EA5E9',
|
backgroundColor: '#7a5af8', // 紫色主题
|
||||||
opacity: 0.05,
|
opacity: 0.04,
|
||||||
},
|
},
|
||||||
headerLeft: {
|
headerLeft: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -2177,6 +2199,11 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
},
|
},
|
||||||
historyButton: {
|
historyButton: {
|
||||||
width: 32,
|
width: 32,
|
||||||
@@ -2237,7 +2264,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: `${Colors.light.accentGreen}99` // 60% opacity
|
backgroundColor: '#7a5af899' // 紫色主题 60% opacity
|
||||||
},
|
},
|
||||||
dietOptionsContainer: {
|
dietOptionsContainer: {
|
||||||
gap: 8,
|
gap: 8,
|
||||||
@@ -2249,13 +2276,13 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: `${Colors.light.accentGreen}4D`, // 30% opacity
|
borderColor: '#7a5af84d', // 紫色主题 30% opacity
|
||||||
},
|
},
|
||||||
dietOptionIconContainer: {
|
dietOptionIconContainer: {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
backgroundColor: `${Colors.light.accentGreen}33`, // 20% opacity
|
backgroundColor: '#7a5af833', // 紫色主题 20% opacity
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
@@ -2304,7 +2331,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: `${Colors.light.accentGreen}99`, // 60% opacity
|
backgroundColor: '#7a5af899', // 紫色主题 60% opacity
|
||||||
alignSelf: 'flex-end',
|
alignSelf: 'flex-end',
|
||||||
},
|
},
|
||||||
// markdown 基础样式承载容器的字体尺寸保持与气泡一致
|
// markdown 基础样式承载容器的字体尺寸保持与气泡一致
|
||||||
@@ -2349,7 +2376,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundColor: 'rgba(0,0,0,0.06)'
|
backgroundColor: 'rgba(122,90,248,0.08)' // 使用紫色主题的浅色背景
|
||||||
},
|
},
|
||||||
imageThumb: {
|
imageThumb: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -2404,7 +2431,7 @@ const styles = StyleSheet.create({
|
|||||||
padding: 8,
|
padding: 8,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
backgroundColor: 'rgba(0,0,0,0.04)'
|
backgroundColor: 'rgba(122,90,248,0.04)' // 使用紫色主题的极浅色背景
|
||||||
},
|
},
|
||||||
mediaBtn: {
|
mediaBtn: {
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -2617,17 +2644,17 @@ const styles = StyleSheet.create({
|
|||||||
choiceButton: {
|
choiceButton: {
|
||||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: `${Colors.light.accentGreen}4D`, // 30% opacity
|
borderColor: '#7a5af84d', // 紫色主题 30% opacity
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
},
|
},
|
||||||
choiceButtonRecommended: {
|
choiceButtonRecommended: {
|
||||||
borderColor: `${Colors.light.accentGreen}99`, // 60% opacity
|
borderColor: '#7a5af899', // 紫色主题 60% opacity
|
||||||
backgroundColor: `${Colors.light.accentGreen}1A`, // 10% opacity
|
backgroundColor: '#7a5af81a', // 紫色主题 10% opacity
|
||||||
},
|
},
|
||||||
choiceButtonSelected: {
|
choiceButtonSelected: {
|
||||||
borderColor: Colors.light.accentGreenDark,
|
borderColor: '#19b36e', // success[500]
|
||||||
backgroundColor: `${Colors.light.accentGreen}33`, // 20% opacity
|
backgroundColor: '#19b36e33', // 20% opacity
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
choiceButtonDisabled: {
|
choiceButtonDisabled: {
|
||||||
@@ -2647,10 +2674,10 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
choiceLabelRecommended: {
|
choiceLabelRecommended: {
|
||||||
color: Colors.light.accentGreenDark,
|
color: '#19b36e', // success[500]
|
||||||
},
|
},
|
||||||
choiceLabelSelected: {
|
choiceLabelSelected: {
|
||||||
color: Colors.light.accentGreenDark,
|
color: '#19b36e', // success[500]
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
choiceLabelDisabled: {
|
choiceLabelDisabled: {
|
||||||
@@ -2662,7 +2689,7 @@ const styles = StyleSheet.create({
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
},
|
},
|
||||||
recommendedBadge: {
|
recommendedBadge: {
|
||||||
backgroundColor: `${Colors.light.accentGreen}CC`, // 80% opacity
|
backgroundColor: '#7a5af8cc', // 紫色主题 80% opacity
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
@@ -2670,10 +2697,10 @@ const styles = StyleSheet.create({
|
|||||||
recommendedText: {
|
recommendedText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: Colors.light.accentGreenDark,
|
color: '#19b36e', // success[500]
|
||||||
},
|
},
|
||||||
selectedBadge: {
|
selectedBadge: {
|
||||||
backgroundColor: Colors.light.accentGreenDark,
|
backgroundColor: '#19b36e', // success[500]
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
@@ -2714,7 +2741,7 @@ const styles = StyleSheet.create({
|
|||||||
padding: 16,
|
padding: 16,
|
||||||
gap: 16,
|
gap: 16,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: `${Colors.light.accentGreen}33`, // 20% opacity
|
borderColor: '#7a5af833', // 紫色主题 20% opacity
|
||||||
},
|
},
|
||||||
dietPlanHeader: {
|
dietPlanHeader: {
|
||||||
gap: 4,
|
gap: 4,
|
||||||
@@ -2834,7 +2861,7 @@ const styles = StyleSheet.create({
|
|||||||
caloriesValue: {
|
caloriesValue: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
color: Colors.light.accentGreenDark,
|
color: '#19b36e', // success[500]
|
||||||
},
|
},
|
||||||
nutritionGrid: {
|
nutritionGrid: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -2871,7 +2898,7 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
backgroundColor: Colors.light.accentGreenDark,
|
backgroundColor: '#19b36e', // success[500]
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
@@ -2889,7 +2916,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
backgroundColor: 'rgba(0,0,0,0.06)',
|
backgroundColor: 'rgba(122,90,248,0.08)', // 紫色主题浅色背景
|
||||||
},
|
},
|
||||||
usageIcon: {
|
usageIcon: {
|
||||||
width: 16,
|
width: 16,
|
||||||
@@ -2898,7 +2925,7 @@ const styles = StyleSheet.create({
|
|||||||
usageText: {
|
usageText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#687076',
|
color: '#7a5af8', // 紫色主题文字颜色
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/sto
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { Image, Linking, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
import { Image, Linking, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
@@ -31,7 +30,6 @@ export default function PersonalScreen() {
|
|||||||
|
|
||||||
// 颜色主题
|
// 颜色主题
|
||||||
const colors = Colors[colorScheme ?? 'light'];
|
const colors = Colors[colorScheme ?? 'light'];
|
||||||
const theme = (colorScheme ?? 'light') as 'light' | 'dark';
|
|
||||||
|
|
||||||
// 直接使用 Redux 中的用户信息,避免重复状态管理
|
// 直接使用 Redux 中的用户信息,避免重复状态管理
|
||||||
const userProfile = useAppSelector((state) => state.user.profile);
|
const userProfile = useAppSelector((state) => state.user.profile);
|
||||||
@@ -66,129 +64,100 @@ export default function PersonalScreen() {
|
|||||||
// 显示名称
|
// 显示名称
|
||||||
const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME;
|
const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME;
|
||||||
|
|
||||||
// 颜色令牌
|
// 用户信息头部
|
||||||
const colorTokens = colors;
|
const UserHeader = () => (
|
||||||
|
<View style={styles.sectionContainer}>
|
||||||
const UserInfoSection = () => (
|
<Text style={styles.sectionTitle}>个人信息</Text>
|
||||||
<View style={[styles.userInfoCard, { backgroundColor: colorTokens.card }]}>
|
<View style={styles.cardContainer}>
|
||||||
<View style={styles.userInfoContainer}>
|
<View style={styles.userInfoContainer}>
|
||||||
{/* 头像 */}
|
<View style={styles.avatarContainer}>
|
||||||
<View style={styles.avatarContainer}>
|
<Image
|
||||||
<View style={[styles.avatar, { backgroundColor: colorTokens.ornamentAccent }]}>
|
source={{ uri: userProfile.avatar || DEFAULT_AVATAR_URL }}
|
||||||
<Image source={{ uri: userProfile.avatar || DEFAULT_AVATAR_URL }} style={{ width: '100%', height: '100%' }} />
|
style={styles.avatar}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={styles.userDetails}>
|
||||||
|
<Text style={styles.userName}>{displayName}</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
|
||||||
|
<Text style={styles.editButtonText}>编辑</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 用户信息 */}
|
|
||||||
<View style={styles.userDetails}>
|
|
||||||
<Text style={[styles.userName, { color: colorTokens.text }]}>{displayName}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 编辑按钮 */}
|
|
||||||
<TouchableOpacity style={dynamicStyles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
|
|
||||||
<Text style={dynamicStyles.editButtonText}>编辑</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 数据统计部分
|
||||||
const StatsSection = () => (
|
const StatsSection = () => (
|
||||||
<View style={[styles.statsContainer, { backgroundColor: colorTokens.card }]}>
|
<View style={styles.sectionContainer}>
|
||||||
<View style={styles.statItem}>
|
<Text style={styles.sectionTitle}>身体数据</Text>
|
||||||
<Text style={dynamicStyles.statValue}>{formatHeight()}</Text>
|
<View style={styles.cardContainer}>
|
||||||
<Text style={[styles.statLabel, { color: colorTokens.textMuted }]}>身高</Text>
|
<View style={styles.statsContainer}>
|
||||||
</View>
|
<View style={styles.statItem}>
|
||||||
<View style={styles.statItem}>
|
<Text style={styles.statValue}>{formatHeight()}</Text>
|
||||||
<Text style={dynamicStyles.statValue}>{formatWeight()}</Text>
|
<Text style={styles.statLabel}>身高</Text>
|
||||||
<Text style={[styles.statLabel, { color: colorTokens.textMuted }]}>体重</Text>
|
</View>
|
||||||
</View>
|
<View style={styles.statItem}>
|
||||||
<View style={styles.statItem}>
|
<Text style={styles.statValue}>{formatWeight()}</Text>
|
||||||
<Text style={dynamicStyles.statValue}>{formatAge()}</Text>
|
<Text style={styles.statLabel}>体重</Text>
|
||||||
<Text style={[styles.statLabel, { color: colorTokens.textMuted }]}>年龄</Text>
|
</View>
|
||||||
|
<View style={styles.statItem}>
|
||||||
|
<Text style={styles.statValue}>{formatAge()}</Text>
|
||||||
|
<Text style={styles.statLabel}>年龄</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 菜单项组件
|
// 菜单项组件
|
||||||
const MenuSection = ({ title, items }: { title: string; items: any[] }) => (
|
const MenuSection = ({ title, items }: { title: string; items: any[] }) => (
|
||||||
<View style={[styles.menuSection, { backgroundColor: colorTokens.card }]}>
|
<View style={styles.sectionContainer}>
|
||||||
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}>{title}</Text>
|
<Text style={styles.sectionTitle}>{title}</Text>
|
||||||
{items.map((item, index) => (
|
<View style={styles.cardContainer}>
|
||||||
<TouchableOpacity
|
{items.map((item, index) => (
|
||||||
key={index}
|
<TouchableOpacity
|
||||||
style={styles.menuItem}
|
key={index}
|
||||||
onPress={item.type === 'switch' ? undefined : item.onPress}
|
style={[styles.menuItem, index === items.length - 1 && { borderBottomWidth: 0 }]}
|
||||||
disabled={item.type === 'switch'}
|
onPress={item.type === 'switch' ? undefined : item.onPress}
|
||||||
>
|
disabled={item.type === 'switch'}
|
||||||
<View style={styles.menuItemLeft}>
|
>
|
||||||
<View style={[
|
<View style={styles.menuItemLeft}>
|
||||||
styles.menuIcon,
|
<View style={[
|
||||||
{ backgroundColor: item.isDanger ? 'rgba(255,68,68,0.12)' : 'rgba(135,206,235,0.15)' }
|
styles.iconContainer,
|
||||||
]}>
|
{ backgroundColor: item.isDanger ? 'rgba(255,68,68,0.1)' : 'rgba(147, 112, 219, 0.1)' }
|
||||||
<Ionicons
|
]}>
|
||||||
name={item.icon}
|
<Ionicons
|
||||||
size={20}
|
name={item.icon}
|
||||||
color={item.isDanger ? colors.danger : '#4682B4'}
|
size={20}
|
||||||
/>
|
color={item.isDanger ? '#FF4444' : '#9370DB'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.menuItemText}>{item.title}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.menuItemText, { color: colorTokens.text }]}>{item.title}</Text>
|
{item.type === 'switch' ? (
|
||||||
</View>
|
<Switch
|
||||||
{item.type === 'switch' ? (
|
value={isLoggedIn ? notificationEnabled : false}
|
||||||
<Switch
|
onValueChange={(value) => {
|
||||||
value={isLoggedIn ? notificationEnabled : false}
|
if (!isLoggedIn) {
|
||||||
onValueChange={(value) => {
|
pushIfAuthedElseLogin('/profile/notification-settings');
|
||||||
if (!isLoggedIn) {
|
return;
|
||||||
pushIfAuthedElseLogin('/profile/notification-settings');
|
}
|
||||||
return;
|
setNotificationEnabled(value);
|
||||||
}
|
}}
|
||||||
setNotificationEnabled(value);
|
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
|
||||||
}}
|
thumbColor="#FFFFFF"
|
||||||
trackColor={{ false: '#E5E5E5', true: colors.primary }}
|
style={styles.switch}
|
||||||
thumbColor="#FFFFFF"
|
/>
|
||||||
style={styles.switch}
|
) : (
|
||||||
/>
|
<Ionicons name="chevron-forward" size={20} color="#CCCCCC" />
|
||||||
) : (
|
)}
|
||||||
<Ionicons name="chevron-forward" size={20} color={colorTokens.icon} />
|
</TouchableOpacity>
|
||||||
)}
|
))}
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 动态样式
|
|
||||||
const dynamicStyles = StyleSheet.create({
|
|
||||||
editButton: {
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingVertical: 10,
|
|
||||||
borderRadius: 20,
|
|
||||||
},
|
|
||||||
editButtonText: {
|
|
||||||
color: colors.onPrimary,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
statValue: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: colors.primary,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
floatingButton: {
|
|
||||||
width: 56,
|
|
||||||
height: 56,
|
|
||||||
borderRadius: 28,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
shadowColor: colors.primary,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 8,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 菜单项配置
|
// 菜单项配置
|
||||||
const menuSections = [
|
const menuSections = [
|
||||||
{
|
{
|
||||||
@@ -197,25 +166,8 @@ export default function PersonalScreen() {
|
|||||||
{
|
{
|
||||||
icon: 'flag-outline' as const,
|
icon: 'flag-outline' as const,
|
||||||
title: '目标管理',
|
title: '目标管理',
|
||||||
onPress: () => pushIfAuthedElseLogin('/goals'),
|
onPress: () => pushIfAuthedElseLogin('/profile/goals'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: 'telescope-outline' as const,
|
|
||||||
title: '目标管理演示',
|
|
||||||
onPress: () => pushIfAuthedElseLogin('/goal-demo'),
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// icon: 'stats-chart-outline' as const,
|
|
||||||
// title: '训练进度',
|
|
||||||
// onPress: () => {
|
|
||||||
// // 训练进度页面暂未实现,先显示提示
|
|
||||||
// if (isLoggedIn) {
|
|
||||||
// Alert.alert('提示', '训练进度功能正在开发中');
|
|
||||||
// } else {
|
|
||||||
// pushIfAuthedElseLogin('/profile/training-progress');
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -265,12 +217,6 @@ export default function PersonalScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<LinearGradient
|
|
||||||
colors={[colors.backgroundGradientStart, colors.backgroundGradientEnd]}
|
|
||||||
style={styles.gradientBackground}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 0, y: 1 }}
|
|
||||||
/>
|
|
||||||
<StatusBar barStyle={'dark-content'} backgroundColor="transparent" translucent />
|
<StatusBar barStyle={'dark-content'} backgroundColor="transparent" translucent />
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<SafeAreaView style={styles.safeArea}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -278,13 +224,12 @@ export default function PersonalScreen() {
|
|||||||
contentContainerStyle={{ paddingBottom: bottomPadding }}
|
contentContainerStyle={{ paddingBottom: bottomPadding }}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<UserInfoSection />
|
<UserHeader />
|
||||||
<StatsSection />
|
<StatsSection />
|
||||||
<ActivityHeatMap />
|
<ActivityHeatMap />
|
||||||
{menuSections.map((section, index) => (
|
{menuSections.map((section, index) => (
|
||||||
<MenuSection key={index} title={section.title} items={section.items} />
|
<MenuSection key={index} title={section.title} items={section.items} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
@@ -294,48 +239,53 @@ export default function PersonalScreen() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
backgroundColor: '#FAFAFA',
|
||||||
gradientBackground: {
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
},
|
},
|
||||||
safeArea: {
|
safeArea: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
scrollView: {
|
scrollView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 16,
|
||||||
|
paddingTop: 16,
|
||||||
|
},
|
||||||
|
// 部分容器
|
||||||
|
sectionContainer: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#2C3E50',
|
||||||
|
marginBottom: 10,
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
},
|
||||||
|
// 卡片容器
|
||||||
|
cardContainer: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 12,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
// 用户信息区域
|
// 用户信息区域
|
||||||
userInfoCard: {
|
|
||||||
borderRadius: 16,
|
|
||||||
marginBottom: 20,
|
|
||||||
shadowColor: 'rgba(135,206,235,0.3)',
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.15,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 3,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(135,206,235,0.1)',
|
|
||||||
},
|
|
||||||
userInfoContainer: {
|
userInfoContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 20,
|
padding: 16,
|
||||||
},
|
},
|
||||||
avatarContainer: {
|
avatarContainer: {
|
||||||
marginRight: 15,
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
width: 80,
|
width: 60,
|
||||||
height: 80,
|
height: 60,
|
||||||
borderRadius: 40,
|
borderRadius: 30,
|
||||||
alignItems: 'center',
|
borderWidth: 2,
|
||||||
justifyContent: 'center',
|
borderColor: '#9370DB',
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
},
|
||||||
userDetails: {
|
userDetails: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -343,86 +293,75 @@ const styles = StyleSheet.create({
|
|||||||
userName: {
|
userName: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
color: '#2C3E50',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
|
userRole: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#9370DB',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
editButton: {
|
||||||
|
backgroundColor: '#9370DB',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 16,
|
||||||
|
},
|
||||||
|
editButtonText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
// 数据统计
|
||||||
statsContainer: {
|
statsContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
borderRadius: 16,
|
padding: 16,
|
||||||
padding: 20,
|
|
||||||
marginBottom: 20,
|
|
||||||
shadowColor: 'rgba(135,206,235,0.25)',
|
|
||||||
shadowOffset: { width: 0, height: 3 },
|
|
||||||
shadowOpacity: 0.12,
|
|
||||||
shadowRadius: 6,
|
|
||||||
elevation: 3,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(135,206,235,0.08)',
|
|
||||||
},
|
},
|
||||||
statItem: {
|
statItem: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
statValue: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#9370DB',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
statLabel: {
|
statLabel: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
color: '#6C757D',
|
||||||
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
menuSection: {
|
// 菜单项
|
||||||
marginBottom: 20,
|
|
||||||
padding: 16,
|
|
||||||
borderRadius: 16,
|
|
||||||
shadowColor: 'rgba(135,206,235,0.2)',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 4,
|
|
||||||
elevation: 2,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(135,206,235,0.06)',
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: '800',
|
|
||||||
marginBottom: 12,
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
},
|
|
||||||
menuItem: {
|
menuItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingVertical: 16,
|
paddingVertical: 14,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
borderRadius: 12,
|
borderBottomWidth: 1,
|
||||||
marginBottom: 8,
|
borderBottomColor: '#F1F3F4',
|
||||||
},
|
},
|
||||||
menuItemLeft: {
|
menuItemLeft: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
menuIcon: {
|
iconContainer: {
|
||||||
width: 36,
|
width: 32,
|
||||||
height: 36,
|
height: 32,
|
||||||
borderRadius: 8,
|
borderRadius: 6,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
menuItemText: {
|
menuItemText: {
|
||||||
fontSize: 16,
|
fontSize: 15,
|
||||||
flex: 1,
|
color: '#2C3E50',
|
||||||
fontWeight: '600',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
switch: {
|
switch: {
|
||||||
transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }],
|
transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }],
|
||||||
},
|
},
|
||||||
// 浮动按钮
|
|
||||||
floatingButtonContainer: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 30,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
alignItems: 'center',
|
|
||||||
pointerEvents: 'box-none',
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
|||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router } from 'expo-router';
|
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
@@ -120,6 +119,9 @@ export default function ExploreScreen() {
|
|||||||
standHours: 0,
|
standHours: 0,
|
||||||
standHoursGoal: 12
|
standHoursGoal: 12
|
||||||
});
|
});
|
||||||
|
// 血氧饱和度和心率数据
|
||||||
|
const [oxygenSaturation, setOxygenSaturation] = useState<number | null>(null);
|
||||||
|
const [heartRate, setHeartRate] = useState<number | null>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
// 用于触发动画重置的 token(当日期或数据变化时更新)
|
// 用于触发动画重置的 token(当日期或数据变化时更新)
|
||||||
@@ -225,6 +227,13 @@ export default function ExploreScreen() {
|
|||||||
// 更新HRV数据时间
|
// 更新HRV数据时间
|
||||||
setHrvUpdateTime(new Date());
|
setHrvUpdateTime(new Date());
|
||||||
|
|
||||||
|
// 设置血氧饱和度和心率数据
|
||||||
|
setOxygenSaturation(data.oxygenSaturation ?? null);
|
||||||
|
setHeartRate(data.heartRate ?? null);
|
||||||
|
|
||||||
|
console.log('血氧饱和度数据:', data.oxygenSaturation);
|
||||||
|
console.log('心率数据:', data.heartRate);
|
||||||
|
|
||||||
setAnimToken((t) => t + 1);
|
setAnimToken((t) => t + 1);
|
||||||
} else {
|
} else {
|
||||||
console.log('忽略过期健康数据请求结果,key=', requestKey, '最新key=', latestRequestKeyRef.current);
|
console.log('忽略过期健康数据请求结果,key=', requestKey, '最新key=', latestRequestKeyRef.current);
|
||||||
@@ -233,6 +242,9 @@ export default function ExploreScreen() {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('HealthKit流程出现异常:', error);
|
console.error('HealthKit流程出现异常:', error);
|
||||||
|
// 重置血氧饱和度和心率数据
|
||||||
|
setOxygenSaturation(null);
|
||||||
|
setHeartRate(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -389,7 +401,7 @@ export default function ExploreScreen() {
|
|||||||
<FloatingCard style={[styles.masonryCard, styles.moodCard]} delay={1500}>
|
<FloatingCard style={[styles.masonryCard, styles.moodCard]} delay={1500}>
|
||||||
<MoodCard
|
<MoodCard
|
||||||
moodCheckin={currentMoodCheckin}
|
moodCheckin={currentMoodCheckin}
|
||||||
onPress={() => router.push('/mood/calendar')}
|
onPress={() => pushIfAuthedElseLogin('/mood/calendar')}
|
||||||
isLoading={isMoodLoading}
|
isLoading={isMoodLoading}
|
||||||
/>
|
/>
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
@@ -436,6 +448,7 @@ export default function ExploreScreen() {
|
|||||||
<OxygenSaturationCard
|
<OxygenSaturationCard
|
||||||
resetToken={animToken}
|
resetToken={animToken}
|
||||||
style={styles.basalMetabolismCardOverride}
|
style={styles.basalMetabolismCardOverride}
|
||||||
|
oxygenSaturation={oxygenSaturation}
|
||||||
/>
|
/>
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
||||||
@@ -444,6 +457,7 @@ export default function ExploreScreen() {
|
|||||||
<HeartRateCard
|
<HeartRateCard
|
||||||
resetToken={animToken}
|
resetToken={animToken}
|
||||||
style={styles.basalMetabolismCardOverride}
|
style={styles.basalMetabolismCardOverride}
|
||||||
|
heartRate={heartRate}
|
||||||
/>
|
/>
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
import { Colors } from '@/constants/Colors';
|
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import { useRouter } from 'expo-router';
|
|
||||||
import React from 'react';
|
|
||||||
import { SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
||||||
|
|
||||||
export default function GoalDemoScreen() {
|
|
||||||
const router = useRouter();
|
|
||||||
const theme = useColorScheme() ?? 'light';
|
|
||||||
const colorTokens = Colors[theme];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={styles.container}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[
|
|
||||||
colorTokens.backgroundGradientStart,
|
|
||||||
colorTokens.backgroundGradientEnd,
|
|
||||||
]}
|
|
||||||
style={styles.backgroundGradient}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.backButton, { backgroundColor: colorTokens.card }]}
|
|
||||||
onPress={() => router.back()}
|
|
||||||
>
|
|
||||||
<Ionicons name="arrow-back" size={24} color={colorTokens.text} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<Text style={[styles.title, { color: colorTokens.text }]}>
|
|
||||||
目标管理演示
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.demoContainer}>
|
|
||||||
<View style={[styles.demoCard, { backgroundColor: colorTokens.card }]}>
|
|
||||||
<View style={styles.iconContainer}>
|
|
||||||
<Ionicons name="flag" size={48} color={colorTokens.primary} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={[styles.demoTitle, { color: colorTokens.text }]}>
|
|
||||||
智能目标管理系统
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text style={[styles.demoDescription, { color: colorTokens.textSecondary }]}>
|
|
||||||
体验高保真的目标管理界面,包含待办事项卡片滑动、时间筛选器和可滚动时间轴。界面完全按照您的需求设计,支持:
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={styles.featureList}>
|
|
||||||
<View style={styles.featureItem}>
|
|
||||||
<Ionicons name="checkmark-circle" size={16} color={colorTokens.primary} />
|
|
||||||
<Text style={[styles.featureText, { color: colorTokens.textSecondary }]}>
|
|
||||||
横向滑动的待办事项卡片(首屏1.5张)
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.featureItem}>
|
|
||||||
<Ionicons name="checkmark-circle" size={16} color={colorTokens.primary} />
|
|
||||||
<Text style={[styles.featureText, { color: colorTokens.textSecondary }]}>
|
|
||||||
天/周/月时间筛选选择器
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.featureItem}>
|
|
||||||
<Ionicons name="checkmark-circle" size={16} color={colorTokens.primary} />
|
|
||||||
<Text style={[styles.featureText, { color: colorTokens.textSecondary }]}>
|
|
||||||
可滚动的时间轴和任务显示
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.featureItem}>
|
|
||||||
<Ionicons name="checkmark-circle" size={16} color={colorTokens.primary} />
|
|
||||||
<Text style={[styles.featureText, { color: colorTokens.textSecondary }]}>
|
|
||||||
支持同一时间多任务的左右上下滑动
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.enterButton, { backgroundColor: colorTokens.primary }]}
|
|
||||||
onPress={() => router.push('/goals')}
|
|
||||||
>
|
|
||||||
<Text style={[styles.enterButtonText, { color: colorTokens.onPrimary }]}>
|
|
||||||
进入目标管理页面
|
|
||||||
</Text>
|
|
||||||
<Ionicons name="arrow-forward" size={20} color={colorTokens.onPrimary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
backgroundGradient: {
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: 1,
|
|
||||||
padding: 20,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 30,
|
|
||||||
marginTop: 20,
|
|
||||||
},
|
|
||||||
backButton: {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
borderRadius: 20,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: 16,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 4,
|
|
||||||
elevation: 3,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: '800',
|
|
||||||
},
|
|
||||||
demoContainer: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
demoCard: {
|
|
||||||
borderRadius: 24,
|
|
||||||
padding: 32,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 8 },
|
|
||||||
shadowOpacity: 0.15,
|
|
||||||
shadowRadius: 20,
|
|
||||||
elevation: 10,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
iconContainer: {
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
borderRadius: 40,
|
|
||||||
backgroundColor: '#E6F3FF',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 24,
|
|
||||||
},
|
|
||||||
demoTitle: {
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: '700',
|
|
||||||
marginBottom: 16,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
demoDescription: {
|
|
||||||
fontSize: 16,
|
|
||||||
lineHeight: 24,
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: 24,
|
|
||||||
},
|
|
||||||
featureList: {
|
|
||||||
alignSelf: 'stretch',
|
|
||||||
marginBottom: 32,
|
|
||||||
},
|
|
||||||
featureItem: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
featureText: {
|
|
||||||
fontSize: 14,
|
|
||||||
marginLeft: 8,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
enterButton: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: 32,
|
|
||||||
paddingVertical: 16,
|
|
||||||
borderRadius: 28,
|
|
||||||
shadowColor: '#87CEEB',
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 6,
|
|
||||||
},
|
|
||||||
enterButtonText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '700',
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -195,11 +195,15 @@ export default function MoodCalendarScreen() {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={backgroundGradientColors}
|
colors={['#fafaff', '#f4f3ff']} // 使用紫色主题的浅色渐变
|
||||||
style={styles.gradientBackground}
|
style={styles.gradientBackground}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 0, y: 1 }}
|
end={{ x: 0, y: 1 }}
|
||||||
/>
|
/>
|
||||||
|
{/* 装饰性圆圈 */}
|
||||||
|
<View style={styles.decorativeCircle1} />
|
||||||
|
<View style={styles.decorativeCircle2} />
|
||||||
|
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<SafeAreaView style={styles.safeArea}>
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
title="心情日历"
|
title="心情日历"
|
||||||
@@ -345,6 +349,26 @@ const styles = StyleSheet.create({
|
|||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
|
decorativeCircle1: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 40,
|
||||||
|
right: 20,
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
borderRadius: 30,
|
||||||
|
backgroundColor: '#7a5af8',
|
||||||
|
opacity: 0.08,
|
||||||
|
},
|
||||||
|
decorativeCircle2: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -15,
|
||||||
|
left: -15,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: '#7a5af8',
|
||||||
|
opacity: 0.04,
|
||||||
|
},
|
||||||
safeArea: {
|
safeArea: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@@ -353,45 +377,56 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
calendar: {
|
calendar: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
margin: 16,
|
margin: 16,
|
||||||
borderRadius: 16,
|
borderRadius: 20,
|
||||||
padding: 16,
|
padding: 20,
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 6,
|
||||||
},
|
},
|
||||||
monthNavigation: {
|
monthNavigation: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 20,
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
navButton: {
|
navButton: {
|
||||||
width: 40,
|
width: 44,
|
||||||
height: 40,
|
height: 44,
|
||||||
borderRadius: 20,
|
borderRadius: 22,
|
||||||
backgroundColor: '#f8f9fa',
|
backgroundColor: 'rgba(122,90,248,0.1)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
},
|
},
|
||||||
navButtonText: {
|
navButtonText: {
|
||||||
fontSize: 20,
|
fontSize: 24,
|
||||||
color: '#333',
|
color: '#7a5af8',
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
monthTitle: {
|
monthTitle: {
|
||||||
fontSize: 18,
|
fontSize: 20,
|
||||||
fontWeight: '700',
|
fontWeight: '800',
|
||||||
color: '#192126',
|
color: '#192126',
|
||||||
},
|
},
|
||||||
weekHeader: {
|
weekHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
marginBottom: 16,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
weekDay: {
|
weekDay: {
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
color: '#666',
|
color: '#5d6676',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
width: (width - 96) / 7,
|
width: (width - 96) / 7,
|
||||||
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
weekRow: {
|
weekRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -403,19 +438,25 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
dayButton: {
|
dayButton: {
|
||||||
width: 40,
|
width: 44,
|
||||||
height: 40,
|
height: 44,
|
||||||
borderRadius: 20,
|
borderRadius: 22,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
dayButtonSelected: {
|
dayButtonSelected: {
|
||||||
backgroundColor: Colors.light.accentGreen,
|
backgroundColor: '#FFFFFF',
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 6,
|
||||||
|
elevation: 4,
|
||||||
},
|
},
|
||||||
dayButtonToday: {
|
dayButtonToday: {
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: Colors.light.accentGreen,
|
borderColor: '#7a5af8',
|
||||||
},
|
},
|
||||||
dayContent: {
|
dayContent: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -426,142 +467,174 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
dayNumber: {
|
dayNumber: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#999',
|
color: '#777f8c',
|
||||||
fontWeight: '500',
|
fontWeight: '600',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 2,
|
top: 2,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
dayNumberSelected: {
|
dayNumberSelected: {
|
||||||
color: '#FFFFFF',
|
color: '#192126',
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
dayNumberToday: {
|
dayNumberToday: {
|
||||||
color: Colors.light.accentGreen,
|
color: '#7a5af8',
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
dayNumberDisabled: {
|
dayNumberDisabled: {
|
||||||
color: '#ccc',
|
color: '#c0c4ca',
|
||||||
},
|
},
|
||||||
moodIconContainer: {
|
moodIconContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 2,
|
bottom: 2,
|
||||||
width: 20,
|
width: 22,
|
||||||
height: 20,
|
height: 22,
|
||||||
borderRadius: 10,
|
borderRadius: 11,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 1,
|
||||||
},
|
},
|
||||||
moodIcon: {
|
moodIcon: {
|
||||||
width: 16,
|
width: 18,
|
||||||
height: 16,
|
height: 18,
|
||||||
borderRadius: 8,
|
borderRadius: 9,
|
||||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
moodEmoji: {
|
moodEmoji: {
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
},
|
},
|
||||||
defaultMoodIcon: {
|
defaultMoodIcon: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 2,
|
bottom: 2,
|
||||||
width: 20,
|
width: 22,
|
||||||
height: 20,
|
height: 22,
|
||||||
borderRadius: 10,
|
borderRadius: 11,
|
||||||
borderWidth: 1,
|
borderWidth: 1.5,
|
||||||
borderColor: '#ddd',
|
borderColor: 'rgba(122,90,248,0.3)',
|
||||||
borderStyle: 'dashed',
|
borderStyle: 'dashed',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'rgba(122,90,248,0.05)',
|
||||||
},
|
},
|
||||||
defaultMoodEmoji: {
|
defaultMoodEmoji: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
opacity: 0.3,
|
opacity: 0.4,
|
||||||
|
color: '#7a5af8',
|
||||||
},
|
},
|
||||||
selectedDateSection: {
|
selectedDateSection: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
margin: 16,
|
margin: 16,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
borderRadius: 16,
|
borderRadius: 20,
|
||||||
padding: 16,
|
padding: 20,
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 6,
|
||||||
},
|
},
|
||||||
selectedDateHeader: {
|
selectedDateHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 16,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
selectedDateTitle: {
|
selectedDateTitle: {
|
||||||
fontSize: 20,
|
fontSize: 22,
|
||||||
fontWeight: '700',
|
fontWeight: '800',
|
||||||
color: '#192126',
|
color: '#192126',
|
||||||
},
|
},
|
||||||
addMoodButton: {
|
addMoodButton: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 20,
|
||||||
height: 32,
|
height: 36,
|
||||||
borderRadius: 16,
|
borderRadius: 18,
|
||||||
backgroundColor: Colors.light.accentGreen,
|
backgroundColor: '#7a5af8',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 3,
|
||||||
},
|
},
|
||||||
addMoodButtonText: {
|
addMoodButtonText: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
moodRecord: {
|
moodRecord: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
paddingVertical: 12,
|
paddingVertical: 16,
|
||||||
|
backgroundColor: 'rgba(122,90,248,0.05)',
|
||||||
|
borderRadius: 16,
|
||||||
|
paddingHorizontal: 16,
|
||||||
},
|
},
|
||||||
recordIcon: {
|
recordIcon: {
|
||||||
width: 48,
|
width: 52,
|
||||||
height: 48,
|
height: 52,
|
||||||
borderRadius: 24,
|
borderRadius: 26,
|
||||||
backgroundColor: Colors.light.accentGreen,
|
backgroundColor: '#7a5af8',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 12,
|
marginRight: 16,
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
},
|
},
|
||||||
recordContent: {
|
recordContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
recordMood: {
|
recordMood: {
|
||||||
fontSize: 16,
|
fontSize: 18,
|
||||||
color: '#333',
|
color: '#192126',
|
||||||
fontWeight: '500',
|
fontWeight: '700',
|
||||||
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
recordIntensity: {
|
recordIntensity: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#666',
|
color: '#5d6676',
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
recordDescription: {
|
recordDescription: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#666',
|
color: '#5d6676',
|
||||||
marginTop: 4,
|
marginTop: 6,
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
spacer: {
|
spacer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
recordTime: {
|
recordTime: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#999',
|
color: '#777f8c',
|
||||||
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
emptyRecord: {
|
emptyRecord: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 20,
|
paddingVertical: 32,
|
||||||
},
|
},
|
||||||
emptyRecordText: {
|
emptyRecordText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#666',
|
color: '#5d6676',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
emptyRecordSubtext: {
|
emptyRecordSubtext: {
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
color: '#999',
|
color: '#777f8c',
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 18,
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,25 +4,25 @@ import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
|||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { getMoodOptions, MoodType } from '@/services/moodCheckins';
|
import { getMoodOptions, MoodType } from '@/services/moodCheckins';
|
||||||
import {
|
import {
|
||||||
createMoodRecord,
|
createMoodRecord,
|
||||||
deleteMoodRecord,
|
deleteMoodRecord,
|
||||||
fetchDailyMoodCheckins,
|
fetchDailyMoodCheckins,
|
||||||
selectMoodRecordsByDate,
|
selectMoodRecordsByDate,
|
||||||
updateMoodRecord
|
updateMoodRecord
|
||||||
} from '@/store/moodSlice';
|
} from '@/store/moodSlice';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router, useLocalSearchParams } from 'expo-router';
|
import { router, useLocalSearchParams } from 'expo-router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
export default function MoodEditScreen() {
|
export default function MoodEditScreen() {
|
||||||
@@ -163,11 +163,15 @@ export default function MoodEditScreen() {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={backgroundGradientColors}
|
colors={['#fafaff', '#f4f3ff']} // 使用紫色主题的浅色渐变
|
||||||
style={styles.gradientBackground}
|
style={styles.gradientBackground}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 0, y: 1 }}
|
end={{ x: 0, y: 1 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 装饰性圆圈 */}
|
||||||
|
<View style={styles.decorativeCircle1} />
|
||||||
|
<View style={styles.decorativeCircle2} />
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<SafeAreaView style={styles.safeArea}>
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
title={existingMood ? '编辑心情' : '记录心情'}
|
title={existingMood ? '编辑心情' : '记录心情'}
|
||||||
@@ -220,6 +224,7 @@ export default function MoodEditScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.descriptionInput}
|
style={styles.descriptionInput}
|
||||||
placeholder="描述一下你的心情..."
|
placeholder="描述一下你的心情..."
|
||||||
|
placeholderTextColor="#777f8c"
|
||||||
value={description}
|
value={description}
|
||||||
onChangeText={setDescription}
|
onChangeText={setDescription}
|
||||||
multiline
|
multiline
|
||||||
@@ -270,6 +275,26 @@ const styles = StyleSheet.create({
|
|||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
|
decorativeCircle1: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 40,
|
||||||
|
right: 20,
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
borderRadius: 30,
|
||||||
|
backgroundColor: '#7a5af8',
|
||||||
|
opacity: 0.08,
|
||||||
|
},
|
||||||
|
decorativeCircle2: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -15,
|
||||||
|
left: -15,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: '#7a5af8',
|
||||||
|
opacity: 0.04,
|
||||||
|
},
|
||||||
safeArea: {
|
safeArea: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@@ -278,29 +303,39 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
dateSection: {
|
dateSection: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
margin: 16,
|
margin: 16,
|
||||||
borderRadius: 16,
|
borderRadius: 20,
|
||||||
padding: 16,
|
padding: 20,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 6,
|
||||||
},
|
},
|
||||||
dateTitle: {
|
dateTitle: {
|
||||||
fontSize: 24,
|
fontSize: 26,
|
||||||
fontWeight: '700',
|
fontWeight: '800',
|
||||||
color: '#192126',
|
color: '#192126',
|
||||||
},
|
},
|
||||||
moodSection: {
|
moodSection: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
margin: 16,
|
margin: 16,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
borderRadius: 16,
|
borderRadius: 20,
|
||||||
padding: 16,
|
padding: 20,
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 6,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 18,
|
fontSize: 20,
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
color: '#333',
|
color: '#192126',
|
||||||
marginBottom: 16,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
moodOptions: {
|
moodOptions: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -310,114 +345,163 @@ const styles = StyleSheet.create({
|
|||||||
moodOption: {
|
moodOption: {
|
||||||
width: '30%',
|
width: '30%',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 16,
|
paddingVertical: 20,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
borderRadius: 12,
|
borderRadius: 16,
|
||||||
backgroundColor: '#f8f8f8',
|
backgroundColor: 'rgba(122,90,248,0.05)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(122,90,248,0.1)',
|
||||||
},
|
},
|
||||||
selectedMoodOption: {
|
selectedMoodOption: {
|
||||||
backgroundColor: '#e8f5e8',
|
backgroundColor: 'rgba(122,90,248,0.15)',
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: Colors.light.accentGreen,
|
borderColor: '#7a5af8',
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
},
|
},
|
||||||
moodEmoji: {
|
moodEmoji: {
|
||||||
fontSize: 24,
|
fontSize: 28,
|
||||||
marginBottom: 8,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
moodLabel: {
|
moodLabel: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#333',
|
color: '#192126',
|
||||||
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
intensitySection: {
|
intensitySection: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
margin: 16,
|
margin: 16,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
borderRadius: 16,
|
borderRadius: 20,
|
||||||
padding: 16,
|
padding: 20,
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 6,
|
||||||
},
|
},
|
||||||
intensityContainer: {
|
intensityContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
intensityLabel: {
|
intensityLabel: {
|
||||||
fontSize: 16,
|
fontSize: 18,
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
color: '#333',
|
color: '#192126',
|
||||||
marginBottom: 12,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
intensitySlider: {
|
intensitySlider: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
marginBottom: 8,
|
marginBottom: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
},
|
},
|
||||||
intensityDot: {
|
intensityDot: {
|
||||||
width: 20,
|
width: 24,
|
||||||
height: 20,
|
height: 24,
|
||||||
borderRadius: 10,
|
borderRadius: 12,
|
||||||
backgroundColor: '#ddd',
|
backgroundColor: 'rgba(122,90,248,0.1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: 'rgba(122,90,248,0.2)',
|
||||||
},
|
},
|
||||||
intensityDotActive: {
|
intensityDotActive: {
|
||||||
backgroundColor: Colors.light.accentGreen,
|
backgroundColor: '#7a5af8',
|
||||||
|
borderColor: '#7a5af8',
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
},
|
},
|
||||||
intensityLabels: {
|
intensityLabels: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
paddingHorizontal: 10,
|
||||||
},
|
},
|
||||||
intensityLabelText: {
|
intensityLabelText: {
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
color: '#666',
|
color: '#5d6676',
|
||||||
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
descriptionSection: {
|
descriptionSection: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
margin: 16,
|
margin: 16,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
borderRadius: 16,
|
borderRadius: 20,
|
||||||
padding: 16,
|
padding: 20,
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 6,
|
||||||
},
|
},
|
||||||
descriptionInput: {
|
descriptionInput: {
|
||||||
borderWidth: 1,
|
borderWidth: 1.5,
|
||||||
borderColor: '#ddd',
|
borderColor: 'rgba(122,90,248,0.2)',
|
||||||
borderRadius: 8,
|
borderRadius: 12,
|
||||||
padding: 12,
|
padding: 16,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
minHeight: 80,
|
minHeight: 100,
|
||||||
textAlignVertical: 'top',
|
textAlignVertical: 'top',
|
||||||
|
backgroundColor: 'rgba(122,90,248,0.02)',
|
||||||
|
color: '#192126',
|
||||||
},
|
},
|
||||||
characterCount: {
|
characterCount: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#999',
|
color: '#777f8c',
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
marginTop: 4,
|
marginTop: 8,
|
||||||
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
padding: 16,
|
padding: 20,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: -4 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 6,
|
||||||
},
|
},
|
||||||
saveButton: {
|
saveButton: {
|
||||||
backgroundColor: Colors.light.accentGreen,
|
backgroundColor: '#7a5af8',
|
||||||
borderRadius: 12,
|
borderRadius: 16,
|
||||||
paddingVertical: 16,
|
paddingVertical: 18,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 8,
|
marginTop: 12,
|
||||||
|
shadowColor: '#7a5af8',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 3,
|
||||||
},
|
},
|
||||||
deleteButton: {
|
deleteButton: {
|
||||||
backgroundColor: '#F44336',
|
backgroundColor: '#f95555',
|
||||||
borderRadius: 12,
|
borderRadius: 16,
|
||||||
paddingVertical: 16,
|
paddingVertical: 18,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
shadowColor: '#f95555',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 3,
|
||||||
},
|
},
|
||||||
disabledButton: {
|
disabledButton: {
|
||||||
backgroundColor: '#ccc',
|
backgroundColor: '#c0c4ca',
|
||||||
|
shadowOpacity: 0,
|
||||||
|
elevation: 0,
|
||||||
},
|
},
|
||||||
saveButtonText: {
|
saveButtonText: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
deleteButtonText: {
|
deleteButtonText: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { Colors, palette } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||||
@@ -270,9 +270,18 @@ export default function EditProfileScreen() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
|
<SafeAreaView style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
|
||||||
<StatusBar barStyle={'dark-content'} />
|
<StatusBar barStyle={'dark-content'} />
|
||||||
|
|
||||||
|
{/* HeaderBar 放在 ScrollView 外部,确保全宽显示 */}
|
||||||
|
<HeaderBar
|
||||||
|
title="编辑资料"
|
||||||
|
onBack={() => router.back()}
|
||||||
|
withSafeTop={false}
|
||||||
|
transparent={true}
|
||||||
|
variant="elevated"
|
||||||
|
/>
|
||||||
|
|
||||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
|
||||||
<ScrollView contentContainerStyle={{ paddingBottom: 40 }} style={{ paddingHorizontal: 20 }} showsVerticalScrollIndicator={false}>
|
<ScrollView contentContainerStyle={{ paddingBottom: 40 }} style={{ paddingHorizontal: 20 }} showsVerticalScrollIndicator={false}>
|
||||||
<HeaderBar title="编辑资料" onBack={() => router.back()} withSafeTop={false} transparent />
|
|
||||||
|
|
||||||
{/* 头像(带相机蒙层,点击从相册选择) */}
|
{/* 头像(带相机蒙层,点击从相册选择) */}
|
||||||
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 16 }}>
|
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 16 }}>
|
||||||
@@ -561,7 +570,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#F1F5F9',
|
backgroundColor: '#F1F5F9',
|
||||||
},
|
},
|
||||||
modalBtnPrimary: {
|
modalBtnPrimary: {
|
||||||
backgroundColor: palette.primary,
|
backgroundColor: '#7a5af8',
|
||||||
},
|
},
|
||||||
modalBtnText: {
|
modalBtnText: {
|
||||||
color: '#334155',
|
color: '#334155',
|
||||||
@@ -571,15 +580,6 @@ const styles = StyleSheet.create({
|
|||||||
color: '#0F172A',
|
color: '#0F172A',
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
header: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
paddingHorizontal: 0,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
backButton: { padding: 4, width: 32 },
|
|
||||||
headerTitle: { fontSize: 18, fontWeight: '700', color: '#192126' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,19 +59,24 @@ const ActivityHeatMap = () => {
|
|||||||
console.log('generateActivityData', generateActivityData);
|
console.log('generateActivityData', generateActivityData);
|
||||||
|
|
||||||
|
|
||||||
// 根据活跃度计算颜色
|
// 根据活跃度计算颜色 - 优化配色方案
|
||||||
const getActivityColor = (level: number): string => {
|
const getActivityColor = (level: number): string => {
|
||||||
// 由于useColorScheme总是返回'light',我们直接使用浅色主题的颜色
|
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 0:
|
case 0:
|
||||||
return colors.separator; // 使用主题分隔线色
|
// 无活动:使用主题适配的背景色
|
||||||
|
return colors.separator;
|
||||||
case 1:
|
case 1:
|
||||||
|
// 低活动:使用主题主色的浅色版本
|
||||||
|
return 'rgba(122, 90, 248, 0.15)'; // 浅色模式下的浅紫色
|
||||||
case 2:
|
case 2:
|
||||||
return 'rgba(135,206,235,0.4)';
|
// 中等活动:使用主题主色的中等透明度
|
||||||
|
return 'rgba(122, 90, 248, 0.35)'; // 浅色模式下的中等紫色
|
||||||
case 3:
|
case 3:
|
||||||
return 'rgba(135,206,235,0.65)';
|
// 高活动:使用主题主色的较高透明度
|
||||||
|
return 'rgba(122, 90, 248, 0.55)'; // 浅色模式下的较深紫色
|
||||||
case 4:
|
case 4:
|
||||||
default:
|
default:
|
||||||
|
// 最高活动:使用主题主色
|
||||||
return colors.primary;
|
return colors.primary;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -143,14 +148,20 @@ const ActivityHeatMap = () => {
|
|||||||
}, [generateActivityData]);
|
}, [generateActivityData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
<View style={[styles.container, {
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
borderColor: colors.border,
|
||||||
|
shadowColor: 'rgba(122, 90, 248, 0.08)',
|
||||||
|
}]}>
|
||||||
{/* 标题和统计 */}
|
{/* 标题和统计 */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.titleRow}>
|
<View style={styles.titleRow}>
|
||||||
<Text style={[styles.subtitle, { color: colors.textMuted }]}>
|
<Text style={[styles.subtitle, { color: colors.textMuted }]}>
|
||||||
最近6个月活跃 {activityStats.activeDays} 天
|
最近6个月活跃 {activityStats.activeDays} 天
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[styles.statsBadge, { backgroundColor: colors.ornamentPrimary }]}>
|
<View style={[styles.statsBadge, {
|
||||||
|
backgroundColor: 'rgba(122, 90, 248, 0.1)'
|
||||||
|
}]}>
|
||||||
<Text style={[styles.statsText, { color: colors.primary }]}>
|
<Text style={[styles.statsText, { color: colors.primary }]}>
|
||||||
{activityStats.activeRate}%
|
{activityStats.activeRate}%
|
||||||
</Text>
|
</Text>
|
||||||
@@ -238,13 +249,10 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
shadowColor: 'rgba(135,206,235,0.25)',
|
|
||||||
shadowOffset: { width: 0, height: 3 },
|
shadowOffset: { width: 0, height: 3 },
|
||||||
shadowOpacity: 0.12,
|
shadowOpacity: 0.12,
|
||||||
shadowRadius: 6,
|
shadowRadius: 6,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(135,206,235,0.08)',
|
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const HealthDataCard: React.FC<HealthDataCardProps> = ({
|
|||||||
style={[styles.card, style]}
|
style={[styles.card, style]}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
{icon}
|
||||||
|
</View>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.title}>{title}</Text>
|
<Text style={styles.title}>{title}</Text>
|
||||||
<View style={styles.valueContainer}>
|
<View style={styles.valueContainer}>
|
||||||
@@ -53,14 +56,18 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
marginRight: 16,
|
marginRight: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#666',
|
color: '#666',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
valueContainer: {
|
valueContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -68,14 +75,15 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: 'bold',
|
fontWeight: '800',
|
||||||
color: '#333',
|
color: '#192126',
|
||||||
},
|
},
|
||||||
unit: {
|
unit: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#666',
|
color: '#666',
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
marginBottom: 2,
|
marginBottom: 2,
|
||||||
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import HealthDataService from '../../services/healthData';
|
|
||||||
import HealthDataCard from './HealthDataCard';
|
import HealthDataCard from './HealthDataCard';
|
||||||
|
|
||||||
interface HeartRateCardProps {
|
interface HeartRateCardProps {
|
||||||
resetToken: number;
|
resetToken: number;
|
||||||
style?: object;
|
style?: object;
|
||||||
|
heartRate?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeartRateCard: React.FC<HeartRateCardProps> = ({
|
const HeartRateCard: React.FC<HeartRateCardProps> = ({
|
||||||
resetToken,
|
resetToken,
|
||||||
style
|
style,
|
||||||
|
heartRate
|
||||||
}) => {
|
}) => {
|
||||||
const [heartRate, setHeartRate] = useState<number | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchHeartRate = async () => {
|
|
||||||
const data = await HealthDataService.getHeartRate();
|
|
||||||
setHeartRate(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchHeartRate();
|
|
||||||
}, [resetToken]);
|
|
||||||
|
|
||||||
const heartIcon = (
|
const heartIcon = (
|
||||||
<Ionicons name="heart" size={24} color="#EF4444" />
|
<Ionicons name="heart" size={24} color="#EF4444" />
|
||||||
);
|
);
|
||||||
@@ -31,7 +21,7 @@ const HeartRateCard: React.FC<HeartRateCardProps> = ({
|
|||||||
return (
|
return (
|
||||||
<HealthDataCard
|
<HealthDataCard
|
||||||
title="心率"
|
title="心率"
|
||||||
value={heartRate !== null ? heartRate.toString() : '--'}
|
value={heartRate !== null && heartRate !== undefined ? Math.round(heartRate).toString() : '--'}
|
||||||
unit="bpm"
|
unit="bpm"
|
||||||
icon={heartIcon}
|
icon={heartIcon}
|
||||||
style={style}
|
style={style}
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import HealthDataService from '../../services/healthData';
|
|
||||||
import HealthDataCard from './HealthDataCard';
|
import HealthDataCard from './HealthDataCard';
|
||||||
|
|
||||||
interface OxygenSaturationCardProps {
|
interface OxygenSaturationCardProps {
|
||||||
resetToken: number;
|
resetToken: number;
|
||||||
style?: object;
|
style?: object;
|
||||||
|
oxygenSaturation?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
||||||
resetToken,
|
resetToken,
|
||||||
style
|
style,
|
||||||
|
oxygenSaturation
|
||||||
}) => {
|
}) => {
|
||||||
const [oxygenSaturation, setOxygenSaturation] = useState<number | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchOxygenSaturation = async () => {
|
|
||||||
const data = await HealthDataService.getOxygenSaturation();
|
|
||||||
setOxygenSaturation(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchOxygenSaturation();
|
|
||||||
}, [resetToken]);
|
|
||||||
|
|
||||||
const oxygenIcon = (
|
const oxygenIcon = (
|
||||||
<Ionicons name="water" size={24} color="#3B82F6" />
|
<Ionicons name="water" size={24} color="#3B82F6" />
|
||||||
);
|
);
|
||||||
@@ -31,7 +21,7 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
|||||||
return (
|
return (
|
||||||
<HealthDataCard
|
<HealthDataCard
|
||||||
title="血氧饱和度"
|
title="血氧饱和度"
|
||||||
value={oxygenSaturation !== null ? oxygenSaturation.toString() : '--'}
|
value={oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--'}
|
||||||
unit="%"
|
unit="%"
|
||||||
icon={oxygenIcon}
|
icon={oxygenIcon}
|
||||||
style={style}
|
style={style}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type HeaderBarProps = {
|
|||||||
showBottomBorder?: boolean;
|
showBottomBorder?: boolean;
|
||||||
withSafeTop?: boolean;
|
withSafeTop?: boolean;
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
|
variant?: 'default' | 'elevated' | 'minimal';
|
||||||
};
|
};
|
||||||
|
|
||||||
export function HeaderBar({
|
export function HeaderBar({
|
||||||
@@ -24,34 +25,115 @@ export function HeaderBar({
|
|||||||
showBottomBorder = false,
|
showBottomBorder = false,
|
||||||
withSafeTop = true,
|
withSafeTop = true,
|
||||||
transparent = true,
|
transparent = true,
|
||||||
|
variant = 'default',
|
||||||
}: HeaderBarProps) {
|
}: HeaderBarProps) {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const colorScheme = useColorScheme() ?? 'light';
|
const colorScheme = useColorScheme() ?? 'light';
|
||||||
const theme = Colors[tone ?? colorScheme];
|
const theme = Colors[tone ?? colorScheme];
|
||||||
|
|
||||||
|
// 根据变体确定背景色和样式
|
||||||
|
const getBackgroundColor = () => {
|
||||||
|
if (transparent) return 'transparent';
|
||||||
|
|
||||||
|
switch (variant) {
|
||||||
|
case 'elevated':
|
||||||
|
return theme.background;
|
||||||
|
case 'minimal':
|
||||||
|
return theme.background;
|
||||||
|
default:
|
||||||
|
return theme.card;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBackButtonStyle = () => {
|
||||||
|
const baseStyle = [styles.backButton];
|
||||||
|
|
||||||
|
switch (variant) {
|
||||||
|
case 'elevated':
|
||||||
|
return [...baseStyle, {
|
||||||
|
backgroundColor: `${theme.primary}15`, // 15% 透明度
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: `${theme.primary}20`, // 20% 透明度
|
||||||
|
}];
|
||||||
|
case 'minimal':
|
||||||
|
return [...baseStyle, {
|
||||||
|
backgroundColor: `${theme.neutral100}80`, // 80% 透明度
|
||||||
|
}];
|
||||||
|
default:
|
||||||
|
return [...baseStyle, {
|
||||||
|
backgroundColor: `${theme.accentGreen}20`, // 20% 透明度
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBackButtonIconColor = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'elevated':
|
||||||
|
return theme.primary;
|
||||||
|
case 'minimal':
|
||||||
|
return theme.textSecondary;
|
||||||
|
default:
|
||||||
|
return theme.onPrimary;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBorderStyle = () => {
|
||||||
|
if (!showBottomBorder) return {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: variant === 'elevated'
|
||||||
|
? theme.border
|
||||||
|
: `${theme.border}40`, // 40% 透明度
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.header,
|
styles.header,
|
||||||
{
|
{
|
||||||
paddingTop: (withSafeTop ? insets.top : 0) + 8,
|
paddingTop: (withSafeTop ? insets.top : 0) + 8,
|
||||||
backgroundColor: transparent ? 'transparent' : theme.card,
|
backgroundColor: getBackgroundColor(),
|
||||||
borderBottomWidth: showBottomBorder ? 1 : 0,
|
...getBorderStyle(),
|
||||||
borderBottomColor: theme.border,
|
...(variant === 'elevated' && {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 3,
|
||||||
|
elevation: 2,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{onBack ? (
|
{onBack ? (
|
||||||
<TouchableOpacity accessibilityRole="button" onPress={onBack} style={[styles.backButton, { backgroundColor: `${Colors.light.accentGreen}33` }]}>
|
<TouchableOpacity
|
||||||
<Ionicons name="chevron-back" size={20} color={theme.onPrimary} />
|
accessibilityRole="button"
|
||||||
|
onPress={onBack}
|
||||||
|
style={getBackButtonStyle()}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="chevron-back"
|
||||||
|
size={20}
|
||||||
|
color={getBackButtonIconColor()}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<View style={{ width: 32 }} />
|
<View style={{ width: 32 }} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
<View style={styles.titleContainer}>
|
||||||
{typeof title === 'string' ? (
|
{typeof title === 'string' ? (
|
||||||
<Text style={[styles.title, { color: theme.text }]}>{title}</Text>
|
<Text style={[
|
||||||
|
styles.title,
|
||||||
|
{
|
||||||
|
color: theme.text,
|
||||||
|
fontWeight: variant === 'elevated' ? '700' : '800',
|
||||||
|
}
|
||||||
|
]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
title
|
title
|
||||||
)}
|
)}
|
||||||
@@ -68,7 +150,9 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingBottom: 10,
|
paddingBottom: 12,
|
||||||
|
minHeight: 44,
|
||||||
|
width: '100%',
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
width: 32,
|
width: 32,
|
||||||
@@ -76,10 +160,25 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
titleContainer: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: 8,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 20,
|
fontSize: 18,
|
||||||
fontWeight: '800',
|
textAlign: 'center',
|
||||||
|
letterSpacing: -0.3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,138 +1,212 @@
|
|||||||
/**
|
/**
|
||||||
* 应用全局配色规范(来自设计规范图)。
|
* 应用全局配色规范(基于设计规范图)。
|
||||||
* 说明:保持原有导出结构不变,同时扩展更完整的语义令牌与原子调色板。
|
* 包含完整的语义化颜色系统:灰色、紫色、成功色、错误色、警告色和基础色。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 原子调色板(与设计图一致)
|
// 原子调色板(与设计图一致)
|
||||||
export const palette = {
|
export const palette = {
|
||||||
// Primary
|
// 灰色系统 - 中性色,UI设计的基础
|
||||||
primary: '#7A5AF8',
|
gray: {
|
||||||
ink: '#FFFFFF',
|
25: '#fcfcfd',
|
||||||
|
50: '#ebecee',
|
||||||
|
100: '#c0c4ca',
|
||||||
|
200: '#a2a7b0',
|
||||||
|
300: '#777f8c',
|
||||||
|
400: '#5d6676',
|
||||||
|
500: '#344054',
|
||||||
|
600: '#2f3a4c',
|
||||||
|
700: '#252d3c',
|
||||||
|
800: '#1d232e',
|
||||||
|
900: '#161b23',
|
||||||
|
},
|
||||||
|
|
||||||
// Secondary / Neutrals
|
// 紫色系统 - 品牌主色,用于交互元素
|
||||||
neutral100: '#888F92',
|
purple: {
|
||||||
neutral200: '#5E6468',
|
25: '#fafaff',
|
||||||
neutral300: '#384046',
|
50: '#f4f3ff',
|
||||||
|
100: '#ebe9fe',
|
||||||
|
200: '#d9d6fe',
|
||||||
|
300: '#bdb4fe',
|
||||||
|
400: '#9b8afb',
|
||||||
|
500: '#7a5af8',
|
||||||
|
600: '#6938ef',
|
||||||
|
700: '#5925dc',
|
||||||
|
800: '#4a1fb8',
|
||||||
|
900: '#3e1c96',
|
||||||
|
},
|
||||||
|
|
||||||
// Accents
|
// 成功色系统 - 绿色,用于正面反馈
|
||||||
purple: '#A48AED',
|
success: {
|
||||||
red: '#ED4747',
|
25: '#f6fef9',
|
||||||
orange: '#FCC46F',
|
50: '#e8f7f1',
|
||||||
blue: '#7A5AF8', // 更贴近logo背景的天空蓝
|
100: '#b8e7d2',
|
||||||
blueSecondary: '#4682B4', // 钢蓝色,用于选中状态
|
200: '#95dcbc',
|
||||||
green: '#9ceb87', // 温暖的绿色,用于心情日历等
|
300: '#65cc9e',
|
||||||
|
400: '#47c28b',
|
||||||
|
500: '#19b36e',
|
||||||
|
600: '#17a364',
|
||||||
|
700: '#127f4e',
|
||||||
|
800: '#0e623d',
|
||||||
|
900: '#0b4b2e',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 错误色系统 - 红色,用于错误状态和破坏性操作
|
||||||
|
error: {
|
||||||
|
25: '#fffbfa',
|
||||||
|
50: '#feeeee',
|
||||||
|
100: '#fdcaca',
|
||||||
|
200: '#fcb1b1',
|
||||||
|
300: '#fb8d8d',
|
||||||
|
400: '#fa7777',
|
||||||
|
500: '#f95555',
|
||||||
|
600: '#e34d4d',
|
||||||
|
700: '#b13c3c',
|
||||||
|
800: '#892f2f',
|
||||||
|
900: '#692424',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 警告色系统 - 黄色/橙色,用于警告和确认
|
||||||
|
warning: {
|
||||||
|
25: '#fff7ec',
|
||||||
|
50: '#fff7ec',
|
||||||
|
100: '#ffe6c4',
|
||||||
|
200: '#ffd9a8',
|
||||||
|
300: '#ffc880',
|
||||||
|
400: '#ffbd68',
|
||||||
|
500: '#ffad42',
|
||||||
|
600: '#e89d3c',
|
||||||
|
700: '#b57b2f',
|
||||||
|
800: '#8c5f24',
|
||||||
|
900: '#6b491c',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 基础色
|
||||||
|
base: {
|
||||||
|
white: '#ffffff',
|
||||||
|
black: '#1d232e',
|
||||||
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const primaryColor = palette.blue; // 应用主题色
|
// 主色调定义
|
||||||
|
const primaryColor = palette.purple[500]; // 紫色500作为主色
|
||||||
const tintColorLight = primaryColor;
|
const tintColorLight = primaryColor;
|
||||||
const tintColorDark = '#FFFFFF';
|
const tintColorDark = palette.base.white;
|
||||||
|
|
||||||
export const Colors = {
|
export const Colors = {
|
||||||
light: {
|
light: {
|
||||||
// 基础文本/背景(优化对比度)
|
// 基础文本/背景
|
||||||
text: '#1A2027', // 更深的文本色,提高可读性
|
text: palette.gray[900], // 最深灰色用于主要文本
|
||||||
textSecondary: '#4A5568', // 温和的次要文本色
|
textSecondary: palette.gray[600], // 中等灰色用于次要文本
|
||||||
textMuted: '#718096', // 柔和的静音文本色
|
textMuted: palette.gray[400], // 浅灰色用于静音文本
|
||||||
background: '#FFFFFF',
|
background: palette.base.white,
|
||||||
surface: '#FFFFFF',
|
surface: palette.base.white,
|
||||||
card: 'rgba(255,255,255,0.95)', // 半透明卡片,与渐变背景融合
|
card: palette.gray[25], // 最浅灰色用于卡片背景
|
||||||
|
|
||||||
buttonBackground: palette.blue,
|
|
||||||
|
|
||||||
// 品牌与可交互主色
|
// 品牌与可交互主色
|
||||||
tint: tintColorLight,
|
tint: tintColorLight,
|
||||||
primary: primaryColor,
|
primary: primaryColor,
|
||||||
onPrimary: palette.ink, // 与主色搭配的前景色(按钮文字/图标)
|
onPrimary: palette.base.white, // 主色上的文字/图标颜色
|
||||||
|
|
||||||
// 中性色与辅助
|
// 中性色与辅助
|
||||||
neutral100: palette.neutral100,
|
neutral100: palette.gray[100],
|
||||||
neutral200: palette.neutral200,
|
neutral200: palette.gray[200],
|
||||||
neutral300: palette.neutral300,
|
neutral300: palette.gray[300],
|
||||||
|
|
||||||
// 状态/反馈色
|
// 状态/反馈色
|
||||||
success: palette.primary,
|
success: palette.success[500],
|
||||||
warning: palette.orange,
|
successLight: palette.success[100],
|
||||||
danger: palette.red,
|
successDark: palette.success[700],
|
||||||
info: palette.blue,
|
warning: palette.warning[500],
|
||||||
accentPurple: palette.purple,
|
warningLight: palette.warning[100],
|
||||||
accentGreen: palette.green, // 温暖的绿色
|
warningDark: palette.warning[700],
|
||||||
accentGreenDark: palette.blueSecondary, // 深绿色,用于文本和强调
|
danger: palette.error[500],
|
||||||
|
dangerLight: palette.error[100],
|
||||||
|
dangerDark: palette.error[700],
|
||||||
|
info: palette.purple[500],
|
||||||
|
accentPurple: palette.purple[400],
|
||||||
|
accentGreen: palette.success[400],
|
||||||
|
|
||||||
// 日期选择器主题色
|
// 日期选择器主题色
|
||||||
datePickerNormal: palette.blue,
|
datePickerNormal: palette.purple[500],
|
||||||
datePickerSelected: palette.green, // 使用温暖的绿色作为选中状态
|
datePickerSelected: palette.success[500],
|
||||||
|
|
||||||
// 结构色(优化后的蓝色主题)
|
// 结构色
|
||||||
border: 'rgba(135,206,235,0.2)', // 蓝色调边框
|
border: palette.gray[200], // 浅灰色边框
|
||||||
separator: 'rgba(135,206,235,0.15)', // 更淡的分隔线
|
separator: palette.gray[100], // 更浅的分隔线
|
||||||
icon: '#5A6C7D', // 蓝灰色图标
|
icon: palette.gray[500], // 中等灰色图标
|
||||||
|
|
||||||
// Tab 相关(保持兼容)
|
// Tab 相关
|
||||||
tabIconDefault: '#687076',
|
tabIconDefault: palette.gray[400],
|
||||||
tabIconSelected: palette.ink, // tab 激活时的文字/图标颜色(深色,在亮色背景上显示)
|
tabIconSelected: palette.base.white, // 选中时使用白色文字/图标,确保在紫色背景上清晰可见
|
||||||
tabBarBackground: palette.ink, // tab 栏背景色
|
tabBarBackground: palette.base.white,
|
||||||
tabBarActiveBackground: primaryColor, // tab 激活时的背景色
|
tabBarActiveBackground: palette.purple[500], // 使用主色作为选中背景
|
||||||
|
|
||||||
// 页面氛围与装饰(优化后的蓝色配色方案)
|
// 页面氛围与装饰
|
||||||
pageBackgroundEmphasis: '#F0F8FF', // 淡蓝色背景强调
|
pageBackgroundEmphasis: palette.gray[25],
|
||||||
heroSurfaceTint: 'rgba(135,206,235,0.12)', // 更柔和的蓝色调表面色彩
|
heroSurfaceTint: palette.purple[25],
|
||||||
ornamentPrimary: 'rgba(135,206,235,0.15)', // 与主色调和的装饰色
|
ornamentPrimary: palette.purple[100],
|
||||||
ornamentAccent: 'rgba(70,130,180,0.12)', // 钢蓝色装饰,增加层次
|
ornamentAccent: palette.success[100],
|
||||||
|
|
||||||
// 优化的背景渐变色(更柔和的蓝色过渡)
|
// 背景渐变色
|
||||||
backgroundGradientStart: '#E6F3FF', // 更柔和的浅蓝色起始
|
backgroundGradientStart: palette.gray[25],
|
||||||
backgroundGradientEnd: '#FAFCFF', // 带有微蓝调的白色结束
|
backgroundGradientEnd: palette.base.white,
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
// 基础文本/背景
|
// 基础文本/背景
|
||||||
text: '#ECEDEE',
|
text: palette.gray[25], // 最浅灰色用于主要文本
|
||||||
textSecondary: palette.neutral100,
|
textSecondary: palette.gray[100], // 浅灰色用于次要文本
|
||||||
textMuted: '#9BA1A6',
|
textMuted: palette.gray[300], // 中等灰色用于静音文本
|
||||||
background: '#151718',
|
background: palette.gray[900],
|
||||||
surface: '#1A1D1E',
|
surface: palette.gray[800],
|
||||||
card: '#1A1D1E',
|
card: palette.gray[800],
|
||||||
|
|
||||||
// 品牌与可交互主色
|
// 品牌与可交互主色
|
||||||
tint: tintColorDark,
|
tint: tintColorDark,
|
||||||
primary: primaryColor,
|
primary: primaryColor,
|
||||||
onPrimary: palette.ink,
|
onPrimary: palette.base.white,
|
||||||
|
|
||||||
// 中性色与辅助
|
// 中性色与辅助
|
||||||
neutral100: palette.neutral100,
|
neutral100: palette.gray[100],
|
||||||
neutral200: palette.neutral200,
|
neutral200: palette.gray[200],
|
||||||
neutral300: palette.neutral300,
|
neutral300: palette.gray[300],
|
||||||
|
|
||||||
// 状态/反馈色
|
// 状态/反馈色
|
||||||
success: palette.primary,
|
success: palette.success[400], // 深色模式下使用较亮的成功色
|
||||||
warning: palette.orange,
|
successLight: palette.success[200],
|
||||||
danger: palette.red,
|
successDark: palette.success[600],
|
||||||
info: palette.blue,
|
warning: palette.warning[400],
|
||||||
accentPurple: palette.purple,
|
warningLight: palette.warning[200],
|
||||||
accentGreenDark: '#2D5016', // 深绿色,用于文本和强调
|
warningDark: palette.warning[600],
|
||||||
|
danger: palette.error[400],
|
||||||
|
dangerLight: palette.error[200],
|
||||||
|
dangerDark: palette.error[600],
|
||||||
|
info: palette.purple[400],
|
||||||
|
accentPurple: palette.purple[300],
|
||||||
|
accentGreen: palette.success[300],
|
||||||
|
|
||||||
// 日期选择器主题色
|
// 日期选择器主题色
|
||||||
datePickerNormal: palette.blue,
|
datePickerNormal: palette.purple[400],
|
||||||
datePickerSelected: palette.green, // 使用温暖的绿色作为选中状态
|
datePickerSelected: palette.success[400],
|
||||||
|
|
||||||
// 结构色
|
// 结构色
|
||||||
border: '#2A2F32',
|
border: palette.gray[700],
|
||||||
separator: '#2A2F32',
|
separator: palette.gray[700],
|
||||||
icon: '#9BA1A6',
|
icon: palette.gray[300],
|
||||||
|
|
||||||
// Tab 相关(保持兼容)
|
// Tab 相关
|
||||||
tabIconDefault: '#9BA1A6',
|
tabIconDefault: palette.gray[400],
|
||||||
tabIconSelected: palette.ink, // 在亮色背景上使用深色文字
|
tabIconSelected: palette.base.white, // 选中时使用白色文字/图标,确保在紫色背景上清晰可见
|
||||||
tabBarBackground: palette.ink,
|
tabBarBackground: palette.gray[800],
|
||||||
tabBarActiveBackground: primaryColor,
|
tabBarActiveBackground: palette.purple[500], // 使用主色作为选中背景,在深色模式下更突出
|
||||||
|
|
||||||
// 页面氛围与装饰(新)
|
// 页面氛围与装饰
|
||||||
pageBackgroundEmphasis: '#151718',
|
pageBackgroundEmphasis: palette.gray[800],
|
||||||
heroSurfaceTint: 'rgba(187,242,70,0.12)',
|
heroSurfaceTint: palette.purple[900],
|
||||||
ornamentPrimary: 'rgba(187,242,70,0.18)',
|
ornamentPrimary: palette.purple[800],
|
||||||
ornamentAccent: 'rgba(164,138,237,0.14)',
|
ornamentAccent: palette.success[800],
|
||||||
|
|
||||||
// 统一背景渐变色(深色模式)
|
// 背景渐变色
|
||||||
backgroundGradientStart: '#0A0B0C', // 深黑色起始
|
backgroundGradientStart: palette.gray[900],
|
||||||
backgroundGradientEnd: '#151718', // 背景色结束
|
backgroundGradientEnd: palette.gray[800],
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
201
docs/headerbar-design-system.md
Normal file
201
docs/headerbar-design-system.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# HeaderBar 组件设计系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
HeaderBar 组件已经与 `Colors.ts` 中的设计系统完全集成,提供了多种变体和优美的 UI 效果。
|
||||||
|
|
||||||
|
## 设计理念
|
||||||
|
|
||||||
|
### 颜色系统集成
|
||||||
|
- 完全基于 `Colors.ts` 中定义的语义化颜色系统
|
||||||
|
- 支持浅色和深色主题自动切换
|
||||||
|
- 使用透明度创建层次感和视觉深度
|
||||||
|
|
||||||
|
### 三种变体设计
|
||||||
|
|
||||||
|
#### 1. Default 变体 (默认)
|
||||||
|
- **背景**: 透明或卡片背景
|
||||||
|
- **返回按钮**: 绿色强调色背景 (20% 透明度)
|
||||||
|
- **图标颜色**: 白色 (确保在绿色背景上清晰可见)
|
||||||
|
- **适用场景**: 标准页面导航
|
||||||
|
|
||||||
|
#### 2. Elevated 变体 (提升)
|
||||||
|
- **背景**: 卡片背景
|
||||||
|
- **返回按钮**: 主色背景 (15% 透明度) + 边框 (20% 透明度)
|
||||||
|
- **图标颜色**: 主色
|
||||||
|
- **边框**: 实线边框
|
||||||
|
- **适用场景**: 重要页面、模态框、需要突出显示的导航
|
||||||
|
|
||||||
|
#### 3. Minimal 变体 (简约)
|
||||||
|
- **背景**: 页面背景色
|
||||||
|
- **返回按钮**: 中性色背景 (80% 透明度)
|
||||||
|
- **图标颜色**: 次要文本色
|
||||||
|
- **适用场景**: 内容页面、列表页面、需要最小化视觉干扰的场景
|
||||||
|
|
||||||
|
## 颜色搭配详解
|
||||||
|
|
||||||
|
### 背景色系统
|
||||||
|
```typescript
|
||||||
|
// 透明背景
|
||||||
|
transparent: 'transparent'
|
||||||
|
|
||||||
|
// 卡片背景 (浅色主题)
|
||||||
|
card: palette.gray[25] // #fcfcfd
|
||||||
|
|
||||||
|
// 页面背景 (浅色主题)
|
||||||
|
background: palette.base.white // #ffffff
|
||||||
|
```
|
||||||
|
|
||||||
|
### 按钮背景色
|
||||||
|
```typescript
|
||||||
|
// Default 变体
|
||||||
|
backgroundColor: `${theme.accentGreen}20` // 绿色 20% 透明度
|
||||||
|
|
||||||
|
// Elevated 变体
|
||||||
|
backgroundColor: `${theme.primary}15` // 主色 15% 透明度
|
||||||
|
borderColor: `${theme.primary}20` // 主色 20% 透明度
|
||||||
|
|
||||||
|
// Minimal 变体
|
||||||
|
backgroundColor: `${theme.neutral100}80` // 中性色 80% 透明度
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文本颜色
|
||||||
|
```typescript
|
||||||
|
// 主标题
|
||||||
|
color: theme.text // 主要文本色
|
||||||
|
|
||||||
|
// 次要文本
|
||||||
|
color: theme.textSecondary // 次要文本色
|
||||||
|
|
||||||
|
// 按钮图标
|
||||||
|
color: theme.onPrimary // 主色上的文字/图标颜色
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 基础用法
|
||||||
|
```tsx
|
||||||
|
// 默认透明背景
|
||||||
|
<HeaderBar
|
||||||
|
title="页面标题"
|
||||||
|
onBack={() => navigation.goBack()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
// 卡片背景
|
||||||
|
<HeaderBar
|
||||||
|
title="页面标题"
|
||||||
|
onBack={() => navigation.goBack()}
|
||||||
|
transparent={false}
|
||||||
|
showBottomBorder={true}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 变体使用
|
||||||
|
```tsx
|
||||||
|
// Elevated 变体 - 重要页面
|
||||||
|
<HeaderBar
|
||||||
|
title="重要操作"
|
||||||
|
onBack={() => navigation.goBack()}
|
||||||
|
variant="elevated"
|
||||||
|
showBottomBorder={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
// Minimal 变体 - 内容页面
|
||||||
|
<HeaderBar
|
||||||
|
title="内容列表"
|
||||||
|
onBack={() => navigation.goBack()}
|
||||||
|
variant="minimal"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义右侧内容
|
||||||
|
```tsx
|
||||||
|
<HeaderBar
|
||||||
|
title="编辑页面"
|
||||||
|
onBack={() => navigation.goBack()}
|
||||||
|
right={
|
||||||
|
<TouchableOpacity onPress={handleSave}>
|
||||||
|
<Text style={{ color: theme.primary }}>保存</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
}
|
||||||
|
variant="elevated"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义标题
|
||||||
|
```tsx
|
||||||
|
<HeaderBar
|
||||||
|
title={
|
||||||
|
<View>
|
||||||
|
<Text style={{ color: theme.text, fontSize: 18, fontWeight: '700' }}>
|
||||||
|
主标题
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: theme.textSecondary, fontSize: 12 }}>
|
||||||
|
副标题
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
onBack={() => navigation.goBack()}
|
||||||
|
variant="elevated"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 视觉层次
|
||||||
|
|
||||||
|
### 阴影效果
|
||||||
|
- 返回按钮添加了微妙的阴影效果
|
||||||
|
- 增强按钮的立体感和可点击性
|
||||||
|
- 在浅色和深色主题下都有良好的表现
|
||||||
|
|
||||||
|
### 间距系统
|
||||||
|
- 使用 8px 的基础间距单位
|
||||||
|
- 标题容器有适当的内边距确保居中
|
||||||
|
- 按钮有足够的触摸区域 (32x32)
|
||||||
|
|
||||||
|
### 字体系统
|
||||||
|
- 标题字体大小: 18px
|
||||||
|
- 字重根据变体调整 (700-800)
|
||||||
|
- 字母间距优化 (-0.3) 提升可读性
|
||||||
|
|
||||||
|
## 主题适配
|
||||||
|
|
||||||
|
### 浅色主题
|
||||||
|
- 使用 `palette.gray[900]` 作为主要文本色
|
||||||
|
- 使用 `palette.gray[25]` 作为卡片背景
|
||||||
|
- 绿色强调色用于默认按钮
|
||||||
|
|
||||||
|
### 深色主题
|
||||||
|
- 使用 `palette.gray[25]` 作为主要文本色
|
||||||
|
- 使用 `palette.gray[800]` 作为卡片背景
|
||||||
|
- 自动调整透明度和对比度
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **选择合适的变体**: 根据页面重要性和视觉需求选择变体
|
||||||
|
2. **保持一致性**: 在同一应用中保持 HeaderBar 风格的一致性
|
||||||
|
3. **考虑可访问性**: 确保颜色对比度符合可访问性标准
|
||||||
|
4. **响应式设计**: 在不同屏幕尺寸下测试显示效果
|
||||||
|
5. **性能优化**: 避免在 HeaderBar 中放置复杂的交互元素
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 颜色计算
|
||||||
|
使用模板字符串和透明度后缀来创建半透明颜色:
|
||||||
|
```typescript
|
||||||
|
`${theme.primary}15` // 15% 透明度
|
||||||
|
`${theme.accentGreen}20` // 20% 透明度
|
||||||
|
```
|
||||||
|
|
||||||
|
### 动态样式
|
||||||
|
根据变体和主题动态计算样式,确保视觉一致性:
|
||||||
|
```typescript
|
||||||
|
const getBackButtonStyle = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'elevated':
|
||||||
|
return { backgroundColor: `${theme.primary}15` };
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
这种设计确保了 HeaderBar 组件与整个应用的设计系统完美融合,提供了优美且一致的用户体验。
|
||||||
@@ -11,6 +11,8 @@ const PERMISSIONS: HealthKitPermissions = {
|
|||||||
AppleHealthKit.Constants.Permissions.SleepAnalysis,
|
AppleHealthKit.Constants.Permissions.SleepAnalysis,
|
||||||
AppleHealthKit.Constants.Permissions.HeartRateVariability,
|
AppleHealthKit.Constants.Permissions.HeartRateVariability,
|
||||||
AppleHealthKit.Constants.Permissions.ActivitySummary,
|
AppleHealthKit.Constants.Permissions.ActivitySummary,
|
||||||
|
AppleHealthKit.Constants.Permissions.OxygenSaturation,
|
||||||
|
AppleHealthKit.Constants.Permissions.HeartRate,
|
||||||
],
|
],
|
||||||
write: [
|
write: [
|
||||||
// 支持体重写入
|
// 支持体重写入
|
||||||
@@ -32,6 +34,9 @@ export type TodayHealthData = {
|
|||||||
exerciseMinutesGoal: number;
|
exerciseMinutesGoal: number;
|
||||||
standHours: number;
|
standHours: number;
|
||||||
standHoursGoal: number;
|
standHoursGoal: number;
|
||||||
|
// 新增血氧饱和度和心率数据
|
||||||
|
oxygenSaturation: number | null;
|
||||||
|
heartRate: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function ensureHealthPermissions(): Promise<boolean> {
|
export async function ensureHealthPermissions(): Promise<boolean> {
|
||||||
@@ -74,8 +79,8 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
|||||||
|
|
||||||
console.log('查询选项:', options);
|
console.log('查询选项:', options);
|
||||||
|
|
||||||
// 并行获取所有健康数据,包括ActivitySummary
|
// 并行获取所有健康数据,包括ActivitySummary、血氧饱和度和心率
|
||||||
const [steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary] = await Promise.all([
|
const [steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary, oxygenSaturation, heartRate] = await Promise.all([
|
||||||
// 获取步数
|
// 获取步数
|
||||||
new Promise<number>((resolve) => {
|
new Promise<number>((resolve) => {
|
||||||
AppleHealthKit.getStepCount({
|
AppleHealthKit.getStepCount({
|
||||||
@@ -198,10 +203,68 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
|||||||
resolve(results[0]);
|
resolve(results[0]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 获取血氧饱和度数据
|
||||||
|
new Promise<number | null>((resolve) => {
|
||||||
|
AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('获取血氧饱和度失败:', err);
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
if (!res || !Array.isArray(res) || res.length === 0) {
|
||||||
|
console.warn('血氧饱和度数据为空或格式错误');
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
console.log('血氧饱和度数据:', res);
|
||||||
|
// 获取最新的血氧饱和度值
|
||||||
|
const latestOxygen = res[res.length - 1];
|
||||||
|
if (latestOxygen && latestOxygen.value !== undefined && latestOxygen.value !== null) {
|
||||||
|
// 血氧饱和度通常在0-100之间,验证数据有效性
|
||||||
|
const value = Number(latestOxygen.value);
|
||||||
|
if (value >= 0 && value <= 100) {
|
||||||
|
resolve(Number(value.toFixed(1)));
|
||||||
|
} else {
|
||||||
|
console.warn('血氧饱和度数据异常:', value);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 获取心率数据
|
||||||
|
new Promise<number | null>((resolve) => {
|
||||||
|
AppleHealthKit.getHeartRateSamples(options, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('获取心率失败:', err);
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
if (!res || !Array.isArray(res) || res.length === 0) {
|
||||||
|
console.warn('心率数据为空或格式错误');
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
console.log('心率数据:', res);
|
||||||
|
// 获取最新的心率值
|
||||||
|
const latestHeartRate = res[res.length - 1];
|
||||||
|
if (latestHeartRate && latestHeartRate.value !== undefined && latestHeartRate.value !== null) {
|
||||||
|
// 心率通常在30-200之间,验证数据有效性
|
||||||
|
const value = Number(latestHeartRate.value);
|
||||||
|
if (value >= 30 && value <= 200) {
|
||||||
|
resolve(Math.round(value));
|
||||||
|
} else {
|
||||||
|
console.warn('心率数据异常:', value);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log('指定日期健康数据获取完成:', { steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary });
|
console.log('指定日期健康数据获取完成:', { steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary, oxygenSaturation, heartRate });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
steps,
|
steps,
|
||||||
@@ -215,7 +278,10 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
|||||||
exerciseMinutes: activitySummary?.appleExerciseTime || 0,
|
exerciseMinutes: activitySummary?.appleExerciseTime || 0,
|
||||||
exerciseMinutesGoal: activitySummary?.appleExerciseTimeGoal || 30,
|
exerciseMinutesGoal: activitySummary?.appleExerciseTimeGoal || 30,
|
||||||
standHours: activitySummary?.appleStandHours || 0,
|
standHours: activitySummary?.appleStandHours || 0,
|
||||||
standHoursGoal: activitySummary?.appleStandHoursGoal || 12
|
standHoursGoal: activitySummary?.appleStandHoursGoal || 12,
|
||||||
|
// 血氧饱和度和心率数据
|
||||||
|
oxygenSaturation,
|
||||||
|
heartRate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user