feat(家庭健康): 优化家庭组加入流程,移除自动创建家庭组逻辑

This commit is contained in:
richarjiang
2025-12-04 19:10:05 +08:00
parent a254af92c7
commit f3d4264b53
5 changed files with 193 additions and 62 deletions

View File

@@ -1,7 +1,6 @@
import { HeaderBar } from '@/components/ui/HeaderBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import {
createFamilyGroup,
fetchFamilyGroup,
generateInviteCode,
selectFamilyGroup,
@@ -13,16 +12,16 @@ 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
import {
ActivityIndicator,
Alert,
Modal,
ScrollView,
Share,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -47,11 +46,6 @@ export default function FamilyInviteScreen() {
// 处理邀请按钮点击
const handleInvite = async () => {
try {
// 如果没有家庭组,先创建一个
if (!familyGroup) {
await dispatch(createFamilyGroup('我的家庭')).unwrap();
}
// 生成邀请码
await dispatch(generateInviteCode(24)).unwrap();

View File

@@ -3,20 +3,29 @@ import { BasicInfoTab } from '@/components/health/tabs/BasicInfoTab';
import { CheckupRecordsTab } from '@/components/health/tabs/CheckupRecordsTab';
import { HealthHistoryTab } from '@/components/health/tabs/HealthHistoryTab';
import { MedicalRecordsTab } from '@/components/health/tabs/MedicalRecordsTab';
import { ConfirmationSheet } from '@/components/ui/ConfirmationSheet';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { ROUTES } from '@/constants/Routes';
import { useAppSelector } from '@/hooks/redux';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useI18n } from '@/hooks/useI18n';
import {
fetchFamilyGroup,
joinFamilyGroup,
selectFamilyGroup,
} from '@/store/familyHealthSlice';
import { selectHealthHistoryProgress } from '@/store/healthSlice';
import { DEFAULT_MEMBER_NAME } from '@/store/userSlice';
import { Toast } from '@/utils/toast.utils';
import { Ionicons } from '@expo/vector-icons';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { Stack, useRouter } from 'expo-router';
import React, { useMemo, useState } from 'react';
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function HealthProfileScreen() {
@@ -25,8 +34,19 @@ export default function HealthProfileScreen() {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const { t } = useI18n();
const dispatch = useAppDispatch();
const { ensureLoggedIn } = useAuthGuard();
const glassAvailable = isLiquidGlassAvailable();
const [activeTab, setActiveTab] = useState(0);
const [joinModalVisible, setJoinModalVisible] = useState(false);
const [inviteCodeInput, setInviteCodeInput] = useState('');
const [relationshipInput, setRelationshipInput] = useState('');
const [isJoining, setIsJoining] = useState(false);
const [joinError, setJoinError] = useState<string | null>(null);
// Redux state
const familyGroup = useAppSelector(selectFamilyGroup);
// Mock user data - in a real app this would come from Redux/Context
const userProfile = useAppSelector((state) => state.user.profile);
@@ -59,6 +79,61 @@ export default function HealthProfileScreen() {
return Math.round((filledCount / totalFields) * 100);
}, [userProfile.height, userProfile.weight, userProfile.waistCircumference]);
// 初始化获取家庭组信息
useEffect(() => {
dispatch(fetchFamilyGroup());
}, [dispatch]);
// 重置弹窗状态
useEffect(() => {
if (!joinModalVisible) {
setInviteCodeInput('');
setRelationshipInput('');
setJoinError(null);
}
}, [joinModalVisible]);
// 打开加入弹窗
const handleOpenJoin = useCallback(async () => {
const ok = await ensureLoggedIn();
if (!ok) return;
setJoinModalVisible(true);
}, [ensureLoggedIn]);
// 提交加入家庭组
const handleSubmitJoin = useCallback(async () => {
if (isJoining) return;
const ok = await ensureLoggedIn();
if (!ok) return;
const code = inviteCodeInput.trim().toUpperCase();
const relationship = relationshipInput.trim();
if (!code) {
setJoinError('请输入邀请码');
return;
}
if (!relationship) {
setJoinError('请输入与创建者的关系');
return;
}
setIsJoining(true);
setJoinError(null);
try {
await dispatch(joinFamilyGroup({ inviteCode: code, relationship })).unwrap();
await dispatch(fetchFamilyGroup());
setJoinModalVisible(false);
Toast.success('成功加入家庭组');
} catch (error) {
const message = typeof error === 'string' ? error : '加入失败,请检查邀请码是否正确';
setJoinError(message);
} finally {
setIsJoining(false);
}
}, [dispatch, ensureLoggedIn, inviteCodeInput, isJoining, relationshipInput]);
const gradientColors: [string, string] =
theme === 'dark'
? ['#1f2230', '#10131e']
@@ -107,6 +182,25 @@ export default function HealthProfileScreen() {
transparent
right={
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{/* 加入家庭组按钮 - 仅在未加入家庭组时显示 */}
{!familyGroup && (
<TouchableOpacity activeOpacity={0.85} onPress={handleOpenJoin} style={{ marginRight: 10 }}>
{glassAvailable ? (
<GlassView
style={styles.joinButtonGlass}
glassEffectStyle="regular"
tintColor="rgba(255,255,255,0.18)"
isInteractive
>
<Text style={styles.joinButtonLabel}></Text>
</GlassView>
) : (
<View style={[styles.joinButtonGlass, styles.joinButtonFallback]}>
<Text style={[styles.joinButtonLabel, { color: colorTokens.text }]}></Text>
</View>
)}
</TouchableOpacity>
)}
<TouchableOpacity style={{ marginRight: 12 }}>
<Ionicons name="settings-outline" size={22} color="#1F2937" />
</TouchableOpacity>
@@ -125,7 +219,10 @@ export default function HealthProfileScreen() {
<Image source={{ uri: avatarUrl }} style={styles.miniAvatar} />
<Text style={styles.miniAvatarName}>{displayName}</Text>
</View>
<TouchableOpacity style={styles.addButton}>
<TouchableOpacity
style={styles.addButton}
onPress={() => router.push(ROUTES.HEALTH_FAMILY_INVITE)}
>
<Ionicons name="add" size={16} color="#6B7280" />
</TouchableOpacity>
</View>
@@ -205,7 +302,44 @@ export default function HealthProfileScreen() {
</ScrollView>
{/* Privacy Warning Footer - Removed as requested */}
{/* 加入家庭组弹窗 */}
<ConfirmationSheet
visible={joinModalVisible}
onClose={() => setJoinModalVisible(false)}
onConfirm={handleSubmitJoin}
title="加入家庭组"
description="输入家人分享的邀请码,加入家庭健康管理"
confirmText={isJoining ? '加入中...' : '加入'}
cancelText="取消"
loading={isJoining}
content={
<View style={styles.modalInputWrapper}>
<TextInput
style={styles.modalInput}
placeholder="请输入邀请码"
placeholderTextColor="#9ca3af"
value={inviteCodeInput}
onChangeText={(text) => setInviteCodeInput(text.toUpperCase())}
autoCapitalize="characters"
autoCorrect={false}
keyboardType="default"
maxLength={12}
/>
<TextInput
style={[styles.modalInput, { marginTop: 12 }]}
placeholder="与创建者的关系(如:配偶、父母、子女)"
placeholderTextColor="#9ca3af"
value={relationshipInput}
onChangeText={setRelationshipInput}
autoCorrect={false}
maxLength={20}
/>
{joinError && joinModalVisible ? (
<Text style={styles.modalError}>{joinError}</Text>
) : null}
</View>
}
/>
</View>
);
}
@@ -339,5 +473,45 @@ const styles = StyleSheet.create({
textAlign: 'center',
lineHeight: 18,
},
joinButtonGlass: {
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 16,
minWidth: 60,
alignItems: 'center',
justifyContent: 'center',
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'rgba(255,255,255,0.45)',
},
joinButtonLabel: {
fontSize: 12,
fontWeight: '700',
color: '#0f1528',
letterSpacing: 0.5,
fontFamily: 'AliBold',
},
joinButtonFallback: {
backgroundColor: 'rgba(255,255,255,0.7)',
},
modalInputWrapper: {
borderRadius: 14,
borderWidth: 1,
borderColor: '#e5e7eb',
backgroundColor: '#f8fafc',
paddingHorizontal: 12,
paddingVertical: 10,
gap: 6,
},
modalInput: {
paddingVertical: 12,
fontSize: 16,
fontWeight: '600',
letterSpacing: 0.5,
color: '#0f1528',
},
modalError: {
marginTop: 10,
fontSize: 12,
color: '#ef4444',
},
});

View File

@@ -265,9 +265,9 @@ export default function WeightRecordsPage() {
<Text style={styles.mainStatUnit}>kg</Text>
</View>
<View style={styles.totalLossTag}>
<Ionicons name={totalWeightLoss <= 0 ? "trending-down" : "trending-up"} size={16} color="#ffffff" />
<Ionicons name={totalWeightLoss > 0 ? "trending-down" : "trending-up"} size={16} color="#ffffff" />
<Text style={styles.totalLossText}>
{totalWeightLoss > 0 ? '+' : ''}{totalWeightLoss.toFixed(1)} kg
{totalWeightLoss > 0 ? '-' : totalWeightLoss < 0 ? '+' : ''}{Math.abs(totalWeightLoss).toFixed(1)} kg
</Text>
</View>
</View>