feat: 添加健康咨询页面及相关功能

- 在应用中新增健康咨询页面,展示用户健康数据和建议
- 集成 expo-linear-gradient 以实现页面背景渐变效果
- 更新 package.json 和 package-lock.json,添加 expo-linear-gradient 依赖
- 在首页中添加健康咨询的导航链接
- 修改布局以支持新页面的显示和交互
This commit is contained in:
richarjiang
2025-08-12 14:33:32 +08:00
parent ddcc1320a4
commit 1d45d4d629
5 changed files with 502 additions and 0 deletions

View File

@@ -56,6 +56,8 @@ export default function HomeScreen() {
onPress={() => {
if (workout.title === 'AI体态评估') {
router.push('/ai-posture-assessment');
} else if (workout.title === '认证教练') {
router.push('/health-consultation' as any);
} else {
console.log(`Pressed ${workout.title}`);
}

481
app/health-consultation.tsx Normal file
View File

@@ -0,0 +1,481 @@
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { useThemeColor } from '@/hooks/useThemeColor';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Dimensions,
Pressable,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
View
} from 'react-native';
const { width: screenWidth } = Dimensions.get('window');
// 健康数据项类型
interface HealthItem {
id: string;
title: string;
subtitle: string;
status: 'warning' | 'good' | 'info';
icon: string;
recommendation: string;
value?: string;
color: string;
bgColor: string;
}
// 健康数据
const healthData: HealthItem[] = [
{
id: '1',
title: '运动状态',
subtitle: '本周运动不足',
status: 'warning',
icon: '🏃‍♀️',
recommendation: '建议每天进行30分钟普拉提训练',
value: '2天/周',
color: '#FF6B6B',
bgColor: '#FFE5E5',
},
{
id: '2',
title: '体态评估',
subtitle: '需要进行评估',
status: 'info',
icon: '🧘‍♀️',
recommendation: '进行AI体态评估了解身体状况',
color: '#4ECDC4',
bgColor: '#E5F9F7',
},
{
id: '3',
title: '核心力量',
subtitle: '待加强',
status: 'warning',
icon: '💪',
recommendation: '推荐核心训练课程',
value: '初级',
color: '#FFB84D',
bgColor: '#FFF4E5',
},
{
id: '4',
title: '柔韧性',
subtitle: '良好',
status: 'good',
icon: '🤸‍♀️',
recommendation: '保持每日拉伸习惯',
value: '良好',
color: '#95E1D3',
bgColor: '#E5F9F5',
},
{
id: '5',
title: '平衡能力',
subtitle: '需要提升',
status: 'info',
icon: '⚖️',
recommendation: '尝试单腿站立训练',
color: '#A8E6CF',
bgColor: '#E8F8F0',
},
{
id: '6',
title: '呼吸质量',
subtitle: '待改善',
status: 'warning',
icon: '🌬️',
recommendation: '学习普拉提呼吸法',
color: '#C7CEEA',
bgColor: '#F0F1F8',
},
];
export default function HealthConsultationScreen() {
const router = useRouter();
const primaryColor = useThemeColor({}, 'primary');
const backgroundColor = useThemeColor({}, 'background');
const textColor = useThemeColor({}, 'text');
const [greeting, setGreeting] = useState('');
useEffect(() => {
const hour = new Date().getHours();
if (hour < 12) {
setGreeting('早上好');
} else if (hour < 18) {
setGreeting('下午好');
} else {
setGreeting('晚上好');
}
}, []);
const handleHealthItemPress = (item: HealthItem) => {
// 根据不同的健康项导航到相应页面
if (item.title === '体态评估') {
router.push('/ai-posture-assessment');
} else {
console.log(`点击了 ${item.title}`);
// 可以添加更多导航逻辑
}
};
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor }]}>
<ThemedView style={styles.container}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* 顶部导航栏 */}
<View style={styles.header}>
<Pressable onPress={() => router.back()} style={styles.backButton}>
<Ionicons name="chevron-back" size={24} color={textColor} />
</Pressable>
<ThemedText style={styles.headerTitle}></ThemedText>
<Pressable style={styles.notificationButton}>
<Ionicons name="notifications-outline" size={24} color={textColor} />
</Pressable>
</View>
{/* 教练问候卡片 */}
<LinearGradient
colors={[primaryColor, '#A8E063']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.coachCard}
>
<View style={styles.coachContent}>
<View style={styles.coachInfo}>
<View style={styles.coachAvatar}>
<Text style={styles.coachAvatarEmoji}>👩</Text>
</View>
<View style={styles.coachTextContainer}>
<Text style={styles.coachGreeting}>{greeting}</Text>
<Text style={styles.coachName}> Sarah</Text>
</View>
</View>
<Text style={styles.coachQuestion}></Text>
<Text style={styles.coachSubtext}></Text>
</View>
</LinearGradient>
{/* 快速操作按钮 */}
<View style={styles.quickActions}>
<Pressable style={[styles.actionButton, { backgroundColor: primaryColor }]}>
<Ionicons name="body-outline" size={20} color="#192126" />
<Text style={styles.actionButtonText}></Text>
</Pressable>
<Pressable style={[styles.actionButton, styles.actionButtonOutline]}>
<Ionicons name="chatbubble-outline" size={20} color={textColor} />
<Text style={[styles.actionButtonText, { color: textColor }]}></Text>
</Pressable>
<Pressable style={[styles.actionButton, styles.actionButtonOutline]}>
<Ionicons name="calendar-outline" size={20} color={textColor} />
<Text style={[styles.actionButtonText, { color: textColor }]}></Text>
</Pressable>
</View>
{/* 健康状况标题 */}
<View style={styles.sectionHeader}>
<ThemedText style={styles.sectionTitle}></ThemedText>
<View style={[styles.healthBadge, { backgroundColor: primaryColor }]}>
<Text style={styles.healthBadgeText}></Text>
</View>
</View>
{/* 健康数据网格 */}
<View style={styles.healthGrid}>
{healthData.map((item) => (
<Pressable
key={item.id}
style={[styles.healthCard, { backgroundColor: item.bgColor }]}
onPress={() => handleHealthItemPress(item)}
>
<View style={styles.healthCardHeader}>
<Text style={styles.healthIcon}>{item.icon}</Text>
{item.value && (
<Text style={[styles.healthValue, { color: item.color }]}>
{item.value}
</Text>
)}
</View>
<Text style={styles.healthTitle}>{item.title}</Text>
<Text style={styles.healthSubtitle}>{item.subtitle}</Text>
<View style={styles.healthRecommendation}>
<Ionicons name="bulb-outline" size={12} color={item.color} />
<Text style={[styles.recommendationText, { color: item.color }]} numberOfLines={2}>
{item.recommendation}
</Text>
</View>
<Ionicons
name="arrow-forward-circle"
size={20}
color={item.color}
style={styles.cardArrow}
/>
</Pressable>
))}
</View>
{/* 今日建议 */}
<View style={styles.suggestionSection}>
<ThemedText style={styles.suggestionTitle}></ThemedText>
<View style={[styles.suggestionCard, { backgroundColor: '#F0F8FF' }]}>
<View style={styles.suggestionIcon}>
<Text style={{ fontSize: 24 }}>💡</Text>
</View>
<View style={styles.suggestionContent}>
<Text style={styles.suggestionMainText}>
</Text>
<Text style={styles.suggestionSubText}>
</Text>
<Pressable style={[styles.startButton, { backgroundColor: primaryColor }]}>
<Text style={styles.startButtonText}></Text>
<Ionicons name="arrow-forward" size={16} color="#192126" />
</Pressable>
</View>
</View>
</View>
{/* 底部间距 */}
<View style={styles.bottomSpacing} />
</ScrollView>
</ThemedView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
container: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 12,
},
backButton: {
padding: 8,
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
},
notificationButton: {
padding: 8,
},
coachCard: {
marginHorizontal: 20,
marginTop: 12,
borderRadius: 20,
padding: 24,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
coachContent: {
gap: 16,
},
coachInfo: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
coachAvatar: {
width: 50,
height: 50,
borderRadius: 25,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
coachAvatarEmoji: {
fontSize: 30,
},
coachTextContainer: {
flex: 1,
},
coachGreeting: {
fontSize: 14,
color: '#192126',
opacity: 0.8,
},
coachName: {
fontSize: 16,
fontWeight: '600',
color: '#192126',
},
coachQuestion: {
fontSize: 28,
fontWeight: 'bold',
color: '#192126',
marginTop: 8,
},
coachSubtext: {
fontSize: 14,
color: '#192126',
opacity: 0.7,
},
quickActions: {
flexDirection: 'row',
paddingHorizontal: 20,
marginTop: 20,
gap: 12,
},
actionButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
borderRadius: 12,
gap: 6,
},
actionButtonOutline: {
borderWidth: 1,
borderColor: '#E0E0E0',
},
actionButtonText: {
fontSize: 14,
fontWeight: '500',
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
marginTop: 32,
marginBottom: 16,
},
sectionTitle: {
fontSize: 22,
fontWeight: 'bold',
},
healthBadge: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
},
healthBadgeText: {
fontSize: 12,
fontWeight: '600',
color: '#192126',
},
healthGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
paddingHorizontal: 16,
gap: 12,
},
healthCard: {
width: (screenWidth - 44) / 2,
padding: 16,
borderRadius: 16,
position: 'relative',
},
healthCardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
healthIcon: {
fontSize: 28,
},
healthValue: {
fontSize: 12,
fontWeight: '600',
},
healthTitle: {
fontSize: 16,
fontWeight: '600',
color: '#192126',
marginBottom: 4,
},
healthSubtitle: {
fontSize: 13,
color: '#666',
marginBottom: 12,
},
healthRecommendation: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: 6,
paddingRight: 20,
},
recommendationText: {
fontSize: 11,
flex: 1,
},
cardArrow: {
position: 'absolute',
bottom: 12,
right: 12,
},
suggestionSection: {
paddingHorizontal: 20,
marginTop: 32,
},
suggestionTitle: {
fontSize: 22,
fontWeight: 'bold',
marginBottom: 16,
},
suggestionCard: {
flexDirection: 'row',
padding: 20,
borderRadius: 16,
gap: 16,
},
suggestionIcon: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
suggestionContent: {
flex: 1,
gap: 8,
},
suggestionMainText: {
fontSize: 15,
fontWeight: '500',
color: '#192126',
},
suggestionSubText: {
fontSize: 13,
color: '#666',
},
startButton: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'flex-start',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
marginTop: 8,
gap: 6,
},
startButtonText: {
fontSize: 14,
fontWeight: '600',
color: '#192126',
},
bottomSpacing: {
height: 100,
},
});

View File

@@ -51,6 +51,8 @@ PODS:
- SDWebImageWebPCoder (~> 0.14.6)
- ExpoKeepAwake (14.1.4):
- ExpoModulesCore
- ExpoLinearGradient (14.1.5):
- ExpoModulesCore
- ExpoLinking (7.1.7):
- ExpoModulesCore
- ExpoModulesCore (2.5.0):
@@ -2107,6 +2109,7 @@ DEPENDENCIES:
- ExpoHead (from `../node_modules/expo-router/ios`)
- ExpoImage (from `../node_modules/expo-image/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
- ExpoLinking (from `../node_modules/expo-linking/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
- ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`)
@@ -2227,6 +2230,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-image/ios"
ExpoKeepAwake:
:path: "../node_modules/expo-keep-awake/ios"
ExpoLinearGradient:
:path: "../node_modules/expo-linear-gradient/ios"
ExpoLinking:
:path: "../node_modules/expo-linking/ios"
ExpoModulesCore:
@@ -2404,6 +2409,7 @@ SPEC CHECKSUMS:
ExpoHead: a7b66cbaeeb51f4a85338d335a0f5467e29a2c90
ExpoImage: e4102c93d1dbe99ff54b075452d1bc9d6ec21b7c
ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7
ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d
ExpoLinking: d5c183998ca6ada66ff45e407e0f965b398a8902
ExpoModulesCore: 00a1b5c73248465bd0b93f59f8538c4573dac579
ExpoSplashScreen: 0ad5acac1b5d2953c6e00d4319f16d616f70d4dd

12
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"expo-font": "~13.3.2",
"expo-haptics": "~14.1.4",
"expo-image": "~2.4.0",
"expo-linear-gradient": "^14.1.5",
"expo-linking": "~7.1.7",
"expo-router": "~5.1.4",
"expo-splash-screen": "~0.30.10",
@@ -6310,6 +6311,17 @@
"react": "*"
}
},
"node_modules/expo-linear-gradient": {
"version": "14.1.5",
"resolved": "https://mirrors.tencent.com/npm/expo-linear-gradient/-/expo-linear-gradient-14.1.5.tgz",
"integrity": "sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-linking": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-7.1.7.tgz",

View File

@@ -23,6 +23,7 @@
"expo-font": "~13.3.2",
"expo-haptics": "~14.1.4",
"expo-image": "~2.4.0",
"expo-linear-gradient": "^14.1.5",
"expo-linking": "~7.1.7",
"expo-router": "~5.1.4",
"expo-splash-screen": "~0.30.10",