feat: 新增健康档案模块,支持家庭邀请与个人健康数据管理

This commit is contained in:
richarjiang
2025-12-04 17:56:04 +08:00
parent e713ffbace
commit a254af92c7
28 changed files with 4177 additions and 315 deletions

View File

@@ -0,0 +1,626 @@
import { HeaderBar } from '@/components/ui/HeaderBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import {
createFamilyGroup,
fetchFamilyGroup,
generateInviteCode,
selectFamilyGroup,
selectFamilyHealthLoading,
selectInviteCode,
selectInviteLoading,
} from '@/store/familyHealthSlice';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { Stack } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
Modal,
ScrollView,
Share,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function FamilyInviteScreen() {
const insets = useSafeAreaInsets();
const dispatch = useAppDispatch();
const [agreed, setAgreed] = useState(true);
const [showQRModal, setShowQRModal] = useState(false);
// Redux state
const familyGroup = useAppSelector(selectFamilyGroup);
const inviteCode = useAppSelector(selectInviteCode);
const isLoading = useAppSelector(selectFamilyHealthLoading);
const isInviteLoading = useAppSelector(selectInviteLoading);
// 初始化时获取家庭组信息
useEffect(() => {
dispatch(fetchFamilyGroup());
}, [dispatch]);
// 处理邀请按钮点击
const handleInvite = async () => {
try {
// 如果没有家庭组,先创建一个
if (!familyGroup) {
await dispatch(createFamilyGroup('我的家庭')).unwrap();
}
// 生成邀请码
await dispatch(generateInviteCode(24)).unwrap();
// 显示二维码弹窗
setShowQRModal(true);
} catch (error: any) {
Alert.alert('邀请失败', error?.message || '请稍后重试');
}
};
// 分享邀请码
const handleShare = async () => {
if (!inviteCode) return;
try {
await Share.share({
message: `邀请您加入我的家庭健康管理组!\n邀请码${inviteCode.inviteCode}\n有效期至${new Date(inviteCode.expiresAt).toLocaleString()}`,
title: '家庭健康管理邀请',
});
} catch (error) {
console.error('分享失败:', error);
}
};
return (
<View style={styles.container}>
<Stack.Screen options={{ headerShown: false }} />
<HeaderBar title="" transparent />
<LinearGradient
colors={['#Eef2FF', '#F5F3FF', '#FFFFFF']}
style={styles.background}
/>
<ScrollView
contentContainerStyle={[styles.scrollContent, { paddingTop: insets.top + 40 }]}
showsVerticalScrollIndicator={false}
>
{/* Header Title Area */}
<View style={styles.headerSection}>
<Text style={styles.mainTitle}></Text>
<Text style={styles.mainTitle}></Text>
<View style={styles.subtitleBadge}>
<Ionicons name="home" size={12} color="#5B4CFF" />
<Text style={styles.subtitleText}></Text>
</View>
</View>
{/* Hero Image / House Icon Area */}
<View style={styles.heroContainer}>
{/* Floating Labels */}
<View style={[styles.floatingLabel, styles.labelLeft]}>
<Text style={styles.floatingLabelText}></Text>
<View style={styles.dot} />
</View>
<View style={[styles.floatingLabel, styles.labelRight]}>
<View style={styles.dot} />
<Text style={styles.floatingLabelText}></Text>
</View>
{/* Main 3D House Icon Placeholder */}
<View style={styles.houseIconPlaceholder}>
<LinearGradient
colors={['#A78BFA', '#5B4CFF']}
style={styles.houseIconGradient}
>
<Ionicons name="heart" size={60} color="#FFFFFF" />
</LinearGradient>
</View>
</View>
{/* Features Grid */}
<View style={styles.featuresCard}>
<View style={styles.featureItem}>
<View style={[styles.featureIcon, { backgroundColor: '#EEF2FF' }]}>
<Ionicons name="share-social" size={24} color="#5B4CFF" />
</View>
<Text style={styles.featureTitle}></Text>
<Text style={styles.featureDesc}></Text>
</View>
<View style={styles.featureItem}>
<View style={[styles.featureIcon, { backgroundColor: '#FEF2F2' }]}>
<Ionicons name="alert-circle" size={24} color="#EF4444" />
</View>
<Text style={styles.featureTitle}></Text>
<Text style={styles.featureDesc}></Text>
</View>
<View style={styles.featureItem}>
<View style={[styles.featureIcon, { backgroundColor: '#FFF7ED' }]}>
<Ionicons name="medkit" size={24} color="#F97316" />
</View>
<Text style={styles.featureTitle}></Text>
<Text style={styles.featureDesc}></Text>
</View>
</View>
{/* Steps Section */}
<View style={styles.stepsContainer}>
<Text style={styles.stepsTitle}>3</Text>
<View style={styles.stepsSubtitleContainer}>
<Text style={styles.stepsSubtitle}>624</Text>
</View>
<View style={styles.stepsRow}>
<View style={styles.stepItem}>
<Text style={styles.stepNumber}>1</Text>
<Text style={styles.stepDesc}></Text>
<View style={styles.stepPhoneMockup}>
<View style={styles.mockupScreen} />
</View>
</View>
<Ionicons name="chevron-forward" size={20} color="#D1D5DB" style={{ marginTop: 40 }} />
<View style={styles.stepItem}>
<Text style={styles.stepNumber}>2</Text>
<Text style={styles.stepDesc}>App</Text>
<View style={styles.stepPhoneMockup}>
<View style={styles.mockupScreen} />
</View>
</View>
<Ionicons name="chevron-forward" size={20} color="#D1D5DB" style={{ marginTop: 40 }} />
<View style={styles.stepItem}>
<Text style={styles.stepNumber}>3</Text>
<Text style={styles.stepDesc}></Text>
<View style={styles.stepPhoneMockup}>
<View style={styles.mockupScreen} />
</View>
</View>
</View>
</View>
{/* Bottom Spacing */}
<View style={{ height: 120 }} />
</ScrollView>
{/* Bottom Action Area */}
<View style={[styles.bottomArea, { paddingBottom: insets.bottom + 16 }]}>
<TouchableOpacity
style={styles.checkboxRow}
onPress={() => setAgreed(!agreed)}
activeOpacity={0.8}
>
<Ionicons
name={agreed ? "checkmark-circle" : "ellipse-outline"}
size={20}
color={agreed ? "#5B4CFF" : "#9CA3AF"}
/>
<Text style={styles.checkboxText}>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.inviteButton, (!agreed || isLoading) && styles.inviteButtonDisabled]}
disabled={!agreed || isLoading}
onPress={handleInvite}
>
{isLoading ? (
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<Text style={styles.inviteButtonText}></Text>
)}
</TouchableOpacity>
</View>
{/* QR Code Modal */}
<Modal
visible={showQRModal}
transparent
animationType="fade"
onRequestClose={() => setShowQRModal(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}></Text>
<TouchableOpacity onPress={() => setShowQRModal(false)}>
<Ionicons name="close" size={24} color="#6B7280" />
</TouchableOpacity>
</View>
{isInviteLoading ? (
<View style={styles.qrContainer}>
<ActivityIndicator size="large" color="#5B4CFF" />
</View>
) : inviteCode ? (
<>
<View style={styles.qrContainer}>
{/* 邀请码大字展示(替代二维码,后续可安装 react-native-qrcode-svg 实现) */}
<View style={styles.inviteCodeDisplay}>
<Ionicons name="qr-code-outline" size={48} color="#5B4CFF" />
<Text style={styles.inviteCodeBig}>{inviteCode.inviteCode}</Text>
<Text style={styles.inviteCodeHint}> App </Text>
</View>
</View>
<View style={styles.inviteCodeContainer}>
<Text style={styles.inviteCodeLabel}></Text>
<Text style={styles.inviteCodeText}>{inviteCode.inviteCode}</Text>
</View>
<Text style={styles.expireText}>
{new Date(inviteCode.expiresAt).toLocaleString()}
</Text>
<TouchableOpacity style={styles.shareButton} onPress={handleShare}>
<Ionicons name="share-outline" size={20} color="#FFFFFF" />
<Text style={styles.shareButtonText}></Text>
</TouchableOpacity>
</>
) : null}
</View>
</View>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F9FAFB',
},
background: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
scrollContent: {
paddingHorizontal: 20,
},
headerSection: {
alignItems: 'center',
marginBottom: 30,
},
mainTitle: {
fontSize: 28,
fontWeight: 'bold',
color: '#1F2937',
lineHeight: 36,
textAlign: 'center',
},
subtitleBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.6)',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
marginTop: 16,
borderWidth: 1,
borderColor: '#FFFFFF',
},
subtitleText: {
fontSize: 12,
color: '#5B4CFF',
marginLeft: 6,
fontWeight: '600',
},
heroContainer: {
alignItems: 'center',
justifyContent: 'center',
height: 180,
marginBottom: 20,
position: 'relative',
},
houseIconPlaceholder: {
width: 140,
height: 140,
alignItems: 'center',
justifyContent: 'center',
},
houseIconGradient: {
width: 100,
height: 100,
borderRadius: 30,
alignItems: 'center',
justifyContent: 'center',
transform: [{ rotate: '45deg' }],
shadowColor: '#5B4CFF',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3,
shadowRadius: 20,
elevation: 10,
},
floatingLabel: {
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.8)',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
labelLeft: {
left: 0,
top: 40,
},
labelRight: {
right: 0,
top: 20,
},
floatingLabelText: {
fontSize: 12,
color: '#6B7280',
fontWeight: '600',
marginHorizontal: 4,
},
dot: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: '#5B4CFF',
},
featuresCard: {
flexDirection: 'row',
backgroundColor: '#FFFFFF',
borderRadius: 20,
padding: 20,
justifyContent: 'space-between',
marginBottom: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.03,
shadowRadius: 8,
elevation: 2,
},
featureItem: {
flex: 1,
alignItems: 'center',
},
featureIcon: {
width: 48,
height: 48,
borderRadius: 24,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
},
featureTitle: {
fontSize: 14,
fontWeight: 'bold',
color: '#1F2937',
marginBottom: 4,
},
featureDesc: {
fontSize: 10,
color: '#9CA3AF',
textAlign: 'center',
},
stepsContainer: {
backgroundColor: 'rgba(255,255,255,0.6)',
borderRadius: 24,
padding: 20,
paddingBottom: 30,
marginBottom: 20,
},
stepsTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1F2937',
textAlign: 'center',
marginBottom: 8,
},
stepsSubtitleContainer: {
backgroundColor: '#F3F4F6',
alignSelf: 'center',
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 10,
marginBottom: 24,
},
stepsSubtitle: {
fontSize: 11,
color: '#6B7280',
},
stepsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
stepItem: {
flex: 1,
alignItems: 'center',
},
stepNumber: {
fontSize: 20,
fontWeight: 'bold',
color: '#5B4CFF',
marginBottom: 8,
fontStyle: 'italic',
},
stepDesc: {
fontSize: 12,
color: '#4B5563',
textAlign: 'center',
marginBottom: 12,
height: 32,
},
stepPhoneMockup: {
width: 60,
height: 100,
backgroundColor: '#FFFFFF',
borderRadius: 10,
borderWidth: 2,
borderColor: '#E5E7EB',
padding: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
mockupScreen: {
flex: 1,
backgroundColor: '#F3F4F6',
borderRadius: 6,
},
bottomArea: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: '#FFFFFF',
paddingTop: 16,
paddingHorizontal: 20,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 10,
},
checkboxRow: {
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: 16,
paddingHorizontal: 4,
},
checkboxText: {
flex: 1,
marginLeft: 8,
fontSize: 12,
color: '#6B7280',
lineHeight: 18,
},
inviteButton: {
backgroundColor: '#5B4CFF',
borderRadius: 28,
height: 56,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#5B4CFF',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
inviteButtonDisabled: {
backgroundColor: '#C4B5FD',
shadowOpacity: 0,
},
inviteButtonText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: 'bold',
},
// Modal styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
modalContent: {
width: '100%',
backgroundColor: '#FFFFFF',
borderRadius: 24,
padding: 24,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#1F2937',
},
qrContainer: {
alignItems: 'center',
justifyContent: 'center',
padding: 20,
backgroundColor: '#F9FAFB',
borderRadius: 16,
marginBottom: 20,
minHeight: 180,
},
inviteCodeDisplay: {
alignItems: 'center',
justifyContent: 'center',
},
inviteCodeBig: {
fontSize: 36,
fontWeight: 'bold',
color: '#5B4CFF',
letterSpacing: 4,
marginTop: 16,
marginBottom: 8,
},
inviteCodeHint: {
fontSize: 12,
color: '#9CA3AF',
},
inviteCodeContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F3F4F6',
borderRadius: 12,
padding: 12,
marginBottom: 12,
},
inviteCodeLabel: {
fontSize: 14,
color: '#6B7280',
marginRight: 8,
},
inviteCodeText: {
fontSize: 20,
fontWeight: 'bold',
color: '#5B4CFF',
letterSpacing: 2,
},
expireText: {
fontSize: 12,
color: '#9CA3AF',
textAlign: 'center',
marginBottom: 20,
},
shareButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#5B4CFF',
borderRadius: 16,
paddingVertical: 14,
},
shareButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
marginLeft: 8,
},
});