feat(个人中心): 优化会员横幅组件,支持深色模式与国际化;新增医疗记录卡片组件,完善健康档案功能

This commit is contained in:
richarjiang
2025-12-05 14:35:10 +08:00
parent f3d4264b53
commit 3d08721474
16 changed files with 3771 additions and 2961 deletions

View File

@@ -16,7 +16,10 @@ import {
joinFamilyGroup,
selectFamilyGroup,
} from '@/store/familyHealthSlice';
import { selectHealthHistoryProgress } from '@/store/healthSlice';
import {
fetchHealthHistory,
selectHealthHistoryProgress
} from '@/store/healthSlice';
import { DEFAULT_MEMBER_NAME } from '@/store/userSlice';
import { Toast } from '@/utils/toast.utils';
import { Ionicons } from '@expo/vector-icons';
@@ -25,7 +28,7 @@ import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { Stack, useRouter } from 'expo-router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { Pressable, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function HealthProfileScreen() {
@@ -41,12 +44,39 @@ export default function HealthProfileScreen() {
const [activeTab, setActiveTab] = useState(0);
const [joinModalVisible, setJoinModalVisible] = useState(false);
const [inviteCodeInput, setInviteCodeInput] = useState('');
const [relationshipInput, setRelationshipInput] = useState('');
const [selectedRelationship, setSelectedRelationship] = useState('');
const [isJoining, setIsJoining] = useState(false);
const [joinError, setJoinError] = useState<string | null>(null);
// Redux state
const familyGroup = useAppSelector(selectFamilyGroup);
const medicalRecords = useAppSelector((state) => state.health.medicalRecords);
const records = medicalRecords?.records || [];
const prescriptions = medicalRecords?.prescriptions || [];
// Calculate Medical Records Count
const medicalRecordsCount = useMemo(() => records.length + prescriptions.length, [records, prescriptions]);
// 亲属关系选项
const relationshipOptions = useMemo(() => [
{ key: 'spouse', label: t('familyGroup.relationships.spouse') },
{ key: 'father', label: t('familyGroup.relationships.father') },
{ key: 'mother', label: t('familyGroup.relationships.mother') },
{ key: 'son', label: t('familyGroup.relationships.son') },
{ key: 'daughter', label: t('familyGroup.relationships.daughter') },
{ key: 'grandfather', label: t('familyGroup.relationships.grandfather') },
{ key: 'grandmother', label: t('familyGroup.relationships.grandmother') },
{ key: 'grandson', label: t('familyGroup.relationships.grandson') },
{ key: 'granddaughter', label: t('familyGroup.relationships.granddaughter') },
{ key: 'brother', label: t('familyGroup.relationships.brother') },
{ key: 'sister', label: t('familyGroup.relationships.sister') },
{ key: 'uncle', label: t('familyGroup.relationships.uncle') },
{ key: 'aunt', label: t('familyGroup.relationships.aunt') },
{ key: 'nephew', label: t('familyGroup.relationships.nephew') },
{ key: 'niece', label: t('familyGroup.relationships.niece') },
{ key: 'cousin', label: t('familyGroup.relationships.cousin') },
{ key: 'other', label: t('familyGroup.relationships.other') },
], [t]);
// Mock user data - in a real app this would come from Redux/Context
const userProfile = useAppSelector((state) => state.user.profile);
@@ -79,16 +109,17 @@ export default function HealthProfileScreen() {
return Math.round((filledCount / totalFields) * 100);
}, [userProfile.height, userProfile.weight, userProfile.waistCircumference]);
// 初始化获取家庭组信息
// 初始化获取家庭组信息和健康史数据
useEffect(() => {
dispatch(fetchFamilyGroup());
dispatch(fetchHealthHistory());
}, [dispatch]);
// 重置弹窗状态
useEffect(() => {
if (!joinModalVisible) {
setInviteCodeInput('');
setRelationshipInput('');
setSelectedRelationship('');
setJoinError(null);
}
}, [joinModalVisible]);
@@ -107,32 +138,34 @@ export default function HealthProfileScreen() {
if (!ok) return;
const code = inviteCodeInput.trim().toUpperCase();
const relationship = relationshipInput.trim();
if (!code) {
setJoinError('请输入邀请码');
setJoinError(t('familyGroup.errors.emptyCode'));
return;
}
if (!relationship) {
setJoinError('请输入与创建者的关系');
if (!selectedRelationship) {
setJoinError(t('familyGroup.errors.emptyRelationship'));
return;
}
// 获取选中关系的显示文本
const relationshipLabel = relationshipOptions.find(r => r.key === selectedRelationship)?.label || selectedRelationship;
setIsJoining(true);
setJoinError(null);
try {
await dispatch(joinFamilyGroup({ inviteCode: code, relationship })).unwrap();
await dispatch(joinFamilyGroup({ inviteCode: code, relationship: relationshipLabel })).unwrap();
await dispatch(fetchFamilyGroup());
setJoinModalVisible(false);
Toast.success('成功加入家庭组');
Toast.success(t('familyGroup.success'));
} catch (error) {
const message = typeof error === 'string' ? error : '加入失败,请检查邀请码是否正确';
setJoinError(message);
} finally {
setIsJoining(false);
}
}, [dispatch, ensureLoggedIn, inviteCodeInput, isJoining, relationshipInput]);
}, [dispatch, ensureLoggedIn, inviteCodeInput, isJoining, selectedRelationship, relationshipOptions, t]);
const gradientColors: [string, string] =
theme === 'dark'
@@ -149,7 +182,7 @@ export default function HealthProfileScreen() {
const tabIcons = ["person", "time", "folder", "clipboard", "medkit"];
const handleTabPress = (index: number) => {
if (index === 4) {
if (index === 3) {
// Handle Medicine Box tab specially
router.push('/medications/manage-medications');
return;
@@ -245,7 +278,7 @@ export default function HealthProfileScreen() {
title={t('health.tabs.healthProfile.medicalRecords')}
progress={0}
gradientColors={['#E0E7FF', '#C7D2FE']}
label="0"
label={medicalRecordsCount.toString()}
suffix="份"
/>
</View>
@@ -307,16 +340,17 @@ export default function HealthProfileScreen() {
visible={joinModalVisible}
onClose={() => setJoinModalVisible(false)}
onConfirm={handleSubmitJoin}
title="加入家庭组"
description="输入家人分享的邀请码,加入家庭健康管理"
confirmText={isJoining ? '加入中...' : '加入'}
cancelText="取消"
title={t('familyGroup.joinTitle')}
description={t('familyGroup.joinDescription')}
confirmText={isJoining ? t('familyGroup.joining') : t('familyGroup.joinButton')}
cancelText={t('familyGroup.cancel')}
loading={isJoining}
content={
<View style={styles.modalInputWrapper}>
<View style={styles.joinModalContent}>
{/* 邀请码输入 */}
<TextInput
style={styles.modalInput}
placeholder="请输入邀请码"
style={styles.inviteCodeInput}
placeholder={t('familyGroup.inviteCodePlaceholder')}
placeholderTextColor="#9ca3af"
value={inviteCodeInput}
onChangeText={(text) => setInviteCodeInput(text.toUpperCase())}
@@ -325,15 +359,43 @@ export default function HealthProfileScreen() {
keyboardType="default"
maxLength={12}
/>
<TextInput
style={[styles.modalInput, { marginTop: 12 }]}
placeholder="与创建者的关系(如:配偶、父母、子女)"
placeholderTextColor="#9ca3af"
value={relationshipInput}
onChangeText={setRelationshipInput}
autoCorrect={false}
maxLength={20}
/>
{/* 关系选择标签 */}
<Text style={styles.relationshipLabel}>{t('familyGroup.relationshipLabel')}</Text>
{/* 关系选项网格 - 固定高度可滚动 */}
<ScrollView
style={styles.relationshipScrollView}
contentContainerStyle={styles.relationshipGrid}
showsVerticalScrollIndicator={true}
nestedScrollEnabled
keyboardShouldPersistTaps="handled"
>
{relationshipOptions.map((option) => {
const isSelected = selectedRelationship === option.key;
return (
<Pressable
key={option.key}
style={[
styles.relationshipChip,
isSelected && styles.relationshipChipSelected,
]}
onPress={() => setSelectedRelationship(option.key)}
>
<Text
style={[
styles.relationshipChipText,
isSelected && styles.relationshipChipTextSelected,
]}
>
{option.label}
</Text>
</Pressable>
);
})}
</ScrollView>
{/* 错误提示 */}
{joinError && joinModalVisible ? (
<Text style={styles.modalError}>{joinError}</Text>
) : null}
@@ -493,24 +555,62 @@ const styles = StyleSheet.create({
joinButtonFallback: {
backgroundColor: 'rgba(255,255,255,0.7)',
},
modalInputWrapper: {
borderRadius: 14,
borderWidth: 1,
borderColor: '#e5e7eb',
backgroundColor: '#f8fafc',
paddingHorizontal: 12,
paddingVertical: 10,
gap: 6,
// 加入家庭组弹窗样式
joinModalContent: {
gap: 12,
},
modalInput: {
paddingVertical: 12,
fontSize: 16,
fontWeight: '600',
letterSpacing: 0.5,
inviteCodeInput: {
backgroundColor: '#f8fafc',
borderRadius: 14,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 18,
fontWeight: '700',
letterSpacing: 2,
color: '#0f1528',
textAlign: 'center',
},
relationshipLabel: {
fontSize: 14,
fontWeight: '600',
color: '#374151',
marginTop: 4,
marginBottom: 2,
},
relationshipScrollView: {
maxHeight: 160,
borderRadius: 12,
backgroundColor: '#fafafa',
},
relationshipGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
padding: 8,
},
relationshipChip: {
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: '#f3f4f6',
borderWidth: 1.5,
borderColor: 'transparent',
},
relationshipChipSelected: {
backgroundColor: '#ede9fe',
borderColor: '#8b5cf6',
},
relationshipChipText: {
fontSize: 14,
color: '#6b7280',
fontWeight: '500',
},
relationshipChipTextSelected: {
color: '#7c3aed',
fontWeight: '600',
},
modalError: {
marginTop: 10,
marginTop: 6,
fontSize: 12,
color: '#ef4444',
},