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,132 @@
import React, { useEffect, useRef } from 'react';
import { Animated, Easing, StyleSheet, Text, View } from 'react-native';
import Svg, { Circle, Defs, LinearGradient, Stop } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
export type HealthProgressRingProps = {
progress: number; // 0-100
size?: number;
strokeWidth?: number;
gradientColors?: string[];
label?: string;
suffix?: string;
title: string;
};
export function HealthProgressRing({
progress,
size = 80,
strokeWidth = 8,
gradientColors = ['#5B4CFF', '#9B8AFB'],
label,
suffix = '%',
title,
}: HealthProgressRingProps) {
const animatedProgress = useRef(new Animated.Value(0)).current;
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const center = size / 2;
useEffect(() => {
Animated.timing(animatedProgress, {
toValue: progress,
duration: 1000,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}).start();
}, [progress]);
const strokeDashoffset = animatedProgress.interpolate({
inputRange: [0, 100],
outputRange: [circumference, 0],
extrapolate: 'clamp',
});
const gradientId = useRef(`grad-${Math.random().toString(36).substr(2, 9)}`).current;
return (
<View style={styles.container}>
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
<Svg width={size} height={size}>
<Defs>
<LinearGradient id={gradientId} x1="0" y1="0" x2="1" y2="1">
<Stop offset="0" stopColor={gradientColors[0]} stopOpacity="1" />
<Stop offset="1" stopColor={gradientColors[1]} stopOpacity="1" />
</LinearGradient>
</Defs>
{/* Background Circle */}
<Circle
cx={center}
cy={center}
r={radius}
stroke="#F3F4F6"
strokeWidth={strokeWidth}
fill="none"
/>
{/* Progress Circle */}
<AnimatedCircle
cx={center}
cy={center}
r={radius}
stroke={`url(#${gradientId})`}
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
transform={`rotate(-90 ${center} ${center})`}
/>
</Svg>
<View style={styles.centerContent}>
<View style={styles.valueContainer}>
<Text style={styles.valueText}>{label ?? progress}</Text>
<Text style={styles.suffixText}>{suffix}</Text>
</View>
</View>
</View>
<Text style={styles.titleText}>{title}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
centerContent: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
},
valueContainer: {
flexDirection: 'row',
alignItems: 'flex-end',
},
valueText: {
fontSize: 20,
fontWeight: 'bold',
color: '#1F2937',
fontFamily: 'AliBold',
lineHeight: 24,
},
suffixText: {
fontSize: 12,
color: '#6B7280',
fontWeight: '500',
marginLeft: 1,
marginBottom: 3,
fontFamily: 'AliRegular',
},
titleText: {
marginTop: 8,
fontSize: 14,
color: '#4B5563', // gray-600
fontFamily: 'AliRegular',
},
});

View File

@@ -0,0 +1,161 @@
import { ROUTES } from '@/constants/Routes';
import { useI18n } from '@/hooks/useI18n';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
type BasicInfoTabProps = {
healthData: {
bmi: string;
height: string;
weight: string;
waist: string;
};
};
export function BasicInfoTab({ healthData }: BasicInfoTabProps) {
const { t } = useI18n();
const router = useRouter();
const handleHeightWeightPress = () => {
router.push(ROUTES.PROFILE_EDIT);
};
const handleWaistPress = () => {
router.push('/circumference-detail');
};
return (
<View style={styles.card}>
<Text style={styles.cardTitle}>{t('health.tabs.healthProfile.basicInfoCard.title')}</Text>
<View style={styles.metricsGrid}>
{/* BMI - Highlighted */}
<View style={styles.metricItemMain}>
<Text style={styles.metricLabelMain}>{t('health.tabs.healthProfile.basicInfoCard.bmi')}</Text>
<Text style={styles.metricValueMain}>
{healthData.bmi === '--' ? t('health.tabs.healthProfile.basicInfoCard.noData') : healthData.bmi}
</Text>
</View>
{/* Height - Clickable */}
<TouchableOpacity
style={styles.metricItem}
onPress={handleHeightWeightPress}
activeOpacity={0.7}
>
<View style={styles.metricHeaderSmall}>
<Text style={styles.metricValue}>{healthData.height}</Text>
<Ionicons name="chevron-forward" size={12} color="#9CA3AF" />
</View>
<Text style={styles.metricLabel}>
{t('health.tabs.healthProfile.basicInfoCard.height')}/{t('health.tabs.healthProfile.basicInfoCard.heightUnit')}
</Text>
</TouchableOpacity>
{/* Weight - Clickable */}
<TouchableOpacity
style={styles.metricItem}
onPress={handleHeightWeightPress}
activeOpacity={0.7}
>
<View style={styles.metricHeaderSmall}>
<Text style={styles.metricValue}>{healthData.weight}</Text>
<Ionicons name="chevron-forward" size={12} color="#9CA3AF" />
</View>
<Text style={styles.metricLabel}>
{t('health.tabs.healthProfile.basicInfoCard.weight')}/{t('health.tabs.healthProfile.basicInfoCard.weightUnit')}
</Text>
</TouchableOpacity>
{/* Waist - Clickable */}
<TouchableOpacity
style={styles.metricItem}
onPress={handleWaistPress}
activeOpacity={0.7}
>
<View style={styles.metricHeaderSmall}>
<Text style={styles.metricValue}>{healthData.waist}</Text>
<Ionicons name="chevron-forward" size={12} color="#9CA3AF" />
</View>
<Text style={styles.metricLabel}>
{t('health.tabs.healthProfile.basicInfoCard.waist')}/{t('health.tabs.healthProfile.basicInfoCard.waistUnit')}
</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#FFFFFF',
borderRadius: 20,
padding: 20,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.03,
shadowRadius: 6,
elevation: 1,
},
cardTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1F2937',
marginBottom: 16,
fontFamily: 'AliBold',
},
metricsGrid: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
metricItemMain: {
flex: 1.5,
backgroundColor: '#F5F3FF',
borderRadius: 12,
padding: 12,
marginRight: 12,
alignItems: 'center',
},
metricHeader: {
flexDirection: 'row',
gap: 2,
marginBottom: 8,
},
metricLabelMain: {
fontSize: 14,
color: '#5B4CFF',
fontWeight: 'bold',
marginBottom: 4,
fontFamily: 'AliBold',
},
metricValueMain: {
fontSize: 16,
color: '#5B4CFF',
fontFamily: 'AliRegular',
},
metricItem: {
flex: 1,
alignItems: 'center',
},
metricHeaderSmall: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
gap: 2,
},
metricLabel: {
fontSize: 11,
color: '#6B7280',
marginBottom: 4,
fontFamily: 'AliRegular',
},
metricValue: {
fontSize: 14,
color: '#1F2937',
fontWeight: '600',
fontFamily: 'AliBold',
},
});

View File

@@ -0,0 +1,49 @@
import { Ionicons } from '@expo/vector-icons';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export function CheckupRecordsTab() {
return (
<View style={styles.card}>
<View style={styles.emptyState}>
<Ionicons name="clipboard-outline" size={48} color="#E5E7EB" />
<Text style={styles.emptyText}></Text>
<Text style={styles.emptySubtext}></Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#FFFFFF',
borderRadius: 20,
padding: 40,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.03,
shadowRadius: 6,
elevation: 1,
minHeight: 200,
alignItems: 'center',
justifyContent: 'center',
},
emptyState: {
alignItems: 'center',
justifyContent: 'center',
},
emptyText: {
marginTop: 16,
fontSize: 16,
fontWeight: '600',
color: '#374151',
fontFamily: 'AliBold',
},
emptySubtext: {
marginTop: 8,
fontSize: 13,
color: '#9CA3AF',
fontFamily: 'AliRegular',
},
});

View File

@@ -0,0 +1,785 @@
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { HealthHistoryCategory } from '@/services/healthProfile';
import {
HistoryItemDetail,
fetchHealthHistory,
saveHealthHistoryCategory,
selectHealthLoading,
selectHistoryData,
updateHistoryData,
} from '@/store/healthSlice';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import DateTimePickerModal from 'react-native-modal-datetime-picker';
import { palette } from '../../../constants/Colors';
// Translation Keys for Recommendations
const RECOMMENDATION_KEYS: Record<string, string[]> = {
allergy: ['penicillin', 'sulfonamides', 'peanuts', 'seafood', 'pollen', 'dustMites', 'alcohol', 'mango'],
disease: ['hypertension', 'diabetes', 'asthma', 'heartDisease', 'gastritis', 'migraine'],
surgery: ['appendectomy', 'cesareanSection', 'tonsillectomy', 'fractureRepair', 'none'],
familyDisease: ['hypertension', 'diabetes', 'cancer', 'heartDisease', 'stroke', 'alzheimers'],
};
interface HistoryItemProps {
title: string;
categoryKey: string;
data: {
hasHistory: boolean | null;
items: HistoryItemDetail[];
};
onPress?: () => void;
}
function HistoryItem({ title, categoryKey, data, onPress }: HistoryItemProps) {
const { t } = useTranslation();
const translateItemName = (name: string) => {
const keys = RECOMMENDATION_KEYS[categoryKey];
if (keys && keys.includes(name)) {
return t(`health.tabs.healthProfile.history.recommendationItems.${categoryKey}.${name}`);
}
return name;
};
const hasItems = data.hasHistory === true && data.items.length > 0;
return (
<TouchableOpacity
style={[styles.itemContainer, hasItems && styles.itemContainerWithList]}
onPress={onPress}
activeOpacity={0.7}
>
{/* Header Row */}
<View style={styles.itemHeader}>
<View style={styles.itemLeft}>
<LinearGradient
colors={[palette.purple[400], palette.purple[600]]}
style={styles.indicator}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
<Text style={styles.itemTitle}>{title}</Text>
</View>
{!hasItems && (
<Text style={[
styles.itemStatus,
(data.hasHistory === true && data.items.length === 0) || data.hasHistory === false ? styles.itemStatusActive : null
]}>
{data.hasHistory === null
? t('health.tabs.healthProfile.history.pending')
: data.hasHistory === false
? t('health.tabs.healthProfile.history.modal.none')
: t('health.tabs.healthProfile.history.modal.yesNoDetails')}
</Text>
)}
</View>
{/* List of Items */}
{hasItems && (
<View style={styles.subListContainer}>
{data.items.map(item => (
<View key={item.id} style={styles.subItemRow}>
<View style={styles.subItemDot} />
<Text style={styles.subItemName}>{translateItemName(item.name)}</Text>
{item.date && (
<Text style={styles.subItemDate}>
{dayjs(item.date).format('YYYY-MM-DD')}
</Text>
)}
</View>
))}
</View>
)}
</TouchableOpacity>
);
}
export function HealthHistoryTab() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
// 从 Redux store 获取健康史数据和加载状态
const historyData = useAppSelector(selectHistoryData);
const isLoading = useAppSelector(selectHealthLoading);
// Modal State
const [modalVisible, setModalVisible] = useState(false);
const [currentType, setCurrentType] = useState<string | null>(null);
const [tempHasHistory, setTempHasHistory] = useState<boolean | null>(null);
const [tempItems, setTempItems] = useState<HistoryItemDetail[]>([]);
const [isSaving, setIsSaving] = useState(false);
// Date Picker State
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const [currentEditingId, setCurrentEditingId] = useState<string | null>(null);
// 初始化时从服务端获取健康史数据
useEffect(() => {
dispatch(fetchHealthHistory());
}, [dispatch]);
const historyItems = [
{ title: t('health.tabs.healthProfile.history.allergy'), key: 'allergy' },
{ title: t('health.tabs.healthProfile.history.disease'), key: 'disease' },
{ title: t('health.tabs.healthProfile.history.surgery'), key: 'surgery' },
{ title: t('health.tabs.healthProfile.history.familyDisease'), key: 'familyDisease' },
];
// Helper to translate item (try to find key, fallback to item itself)
const translateItem = (type: string, item: string) => {
// Check if item is a predefined key
const keys = RECOMMENDATION_KEYS[type];
if (keys && keys.includes(item)) {
return t(`health.tabs.healthProfile.history.recommendationItems.${type}.${item}`);
}
// Fallback for manual input
return item;
};
// Open Modal
const handleItemPress = (key: string) => {
setCurrentType(key);
const currentData = historyData[key];
setTempHasHistory(currentData.hasHistory);
// Deep copy items to avoid reference issues
setTempItems(currentData.items.map(item => ({ ...item })));
setModalVisible(true);
};
// Close Modal
const handleCloseModal = () => {
setModalVisible(false);
setCurrentType(null);
};
// Save Data
const handleSave = async () => {
if (currentType) {
// Filter out empty items
const validItems = tempItems.filter(item => item.name.trim() !== '');
// If "No" history is selected, clear items
const finalItems = tempHasHistory === false ? [] : validItems;
setIsSaving(true);
try {
// 先乐观更新本地状态
dispatch(updateHistoryData({
type: currentType,
data: {
hasHistory: tempHasHistory,
items: finalItems,
},
}));
// 同步到服务端
await dispatch(saveHealthHistoryCategory({
category: currentType as HealthHistoryCategory,
data: {
hasHistory: tempHasHistory ?? false,
items: finalItems.map(item => ({
name: item.name,
date: item.date ? dayjs(item.date).format('YYYY-MM-DD') : undefined,
isRecommendation: item.isRecommendation,
})),
},
})).unwrap();
handleCloseModal();
} catch (error: any) {
// 如果保存失败,显示错误提示(本地数据已更新,下次打开会从服务端同步)
Alert.alert(
t('health.tabs.healthProfile.history.modal.saveError') || '保存失败',
error?.message || '请稍后重试',
[{ text: t('health.tabs.healthProfile.history.modal.ok') || '确定' }]
);
} finally {
setIsSaving(false);
}
}
};
// Add Item (Manual or Recommendation)
const addItem = (name: string = '', isRecommendation: boolean = false) => {
// Avoid duplicates for recommendations if already exists
if (isRecommendation && tempItems.some(item => item.name === name)) {
return;
}
const newItem: HistoryItemDetail = {
id: Date.now().toString() + Math.random().toString(),
name,
isRecommendation
};
setTempItems([...tempItems, newItem]);
};
// Remove Item
const removeItem = (id: string) => {
setTempItems(tempItems.filter(item => item.id !== id));
};
// Update Item Name
const updateItemName = (id: string, text: string) => {
setTempItems(tempItems.map(item =>
item.id === id ? { ...item, name: text } : item
));
};
// Date Picker Handlers
const showDatePicker = (id: string) => {
setCurrentEditingId(id);
setDatePickerVisibility(true);
};
const hideDatePicker = () => {
setDatePickerVisibility(false);
setCurrentEditingId(null);
};
const handleConfirmDate = (date: Date) => {
if (currentEditingId) {
setTempItems(tempItems.map(item =>
item.id === currentEditingId ? { ...item, date: date.toISOString() } : item
));
}
hideDatePicker();
};
return (
<View style={styles.container}>
{/* Glow effect background */}
<View style={styles.glowContainer}>
<View style={styles.glow} />
</View>
<View style={styles.card}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>{t('health.tabs.healthProfile.healthHistory')}</Text>
{isLoading && <ActivityIndicator size="small" color={palette.purple[500]} />}
</View>
{/* List */}
<View style={styles.list}>
{historyItems.map((item) => (
<HistoryItem
key={item.key}
title={item.title}
categoryKey={item.key}
data={historyData[item.key]}
onPress={() => handleItemPress(item.key)}
/>
))}
</View>
</View>
{/* Edit Modal */}
<Modal
animationType="fade"
transparent={true}
visible={modalVisible}
onRequestClose={handleCloseModal}
>
<KeyboardAvoidingView
style={styles.modalOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.modalContent}>
{/* Modal Header */}
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>
{currentType ? t(`health.tabs.healthProfile.history.${currentType}`) : ''}
</Text>
<TouchableOpacity onPress={handleCloseModal} style={styles.closeButton}>
<Ionicons name="close" size={24} color={palette.gray[400]} />
</TouchableOpacity>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Question: Do you have history? */}
<Text style={styles.questionText}>
{t('health.tabs.healthProfile.history.modal.question', {
type: currentType ? t(`health.tabs.healthProfile.history.${currentType}`) : ''
})}
</Text>
<View style={styles.radioGroup}>
<TouchableOpacity
style={[
styles.radioButton,
tempHasHistory === true && styles.radioButtonActive
]}
onPress={() => setTempHasHistory(true)}
>
<Text style={[
styles.radioText,
tempHasHistory === true && styles.radioTextActive
]}>{t('health.tabs.healthProfile.history.modal.yes')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.radioButton,
tempHasHistory === false && styles.radioButtonActive
]}
onPress={() => setTempHasHistory(false)}
>
<Text style={[
styles.radioText,
tempHasHistory === false && styles.radioTextActive
]}>{t('health.tabs.healthProfile.history.modal.no')}</Text>
</TouchableOpacity>
</View>
{/* Conditional Content */}
{tempHasHistory === true && currentType && (
<View style={styles.detailsContainer}>
{/* Recommendations */}
{RECOMMENDATION_KEYS[currentType] && (
<View style={styles.recommendationContainer}>
<Text style={styles.sectionLabel}>{t('health.tabs.healthProfile.history.modal.recommendations')}</Text>
<View style={styles.tagsContainer}>
{RECOMMENDATION_KEYS[currentType].map((tagKey, index) => (
<TouchableOpacity
key={index}
style={styles.tag}
onPress={() => addItem(tagKey, true)}
>
<Text style={styles.tagText}>
{t(`health.tabs.healthProfile.history.recommendationItems.${currentType}.${tagKey}`)}
</Text>
<Ionicons name="add" size={16} color={palette.gray[600]} style={{ marginLeft: 4 }} />
</TouchableOpacity>
))}
</View>
</View>
)}
{/* History List Items */}
<View style={styles.listContainer}>
<Text style={styles.sectionLabel}>{t('health.tabs.healthProfile.history.modal.addDetails')}</Text>
{tempItems.map((item) => (
<View key={item.id} style={styles.listItemCard}>
<View style={styles.listItemHeader}>
<TextInput
style={styles.listItemNameInput}
placeholder={t('health.tabs.healthProfile.history.modal.namePlaceholder')}
placeholderTextColor={palette.gray[300]}
value={item.isRecommendation ? translateItem(currentType!, item.name) : item.name}
onChangeText={(text) => updateItemName(item.id, text)}
editable={!item.isRecommendation}
/>
<TouchableOpacity onPress={() => removeItem(item.id)} style={styles.deleteButton}>
<Ionicons name="trash-outline" size={20} color={palette.error[500]} />
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.datePickerTrigger}
onPress={() => showDatePicker(item.id)}
>
<Ionicons name="calendar-outline" size={18} color={palette.purple[500]} />
<Text style={[
styles.dateText,
!item.date && styles.placeholderText
]}>
{item.date
? dayjs(item.date).format('YYYY-MM-DD')
: t('health.tabs.healthProfile.history.modal.selectDate')}
</Text>
<Ionicons name="chevron-down" size={14} color={palette.gray[400]} style={{ marginLeft: 'auto' }} />
</TouchableOpacity>
</View>
))}
{/* Add Button */}
<TouchableOpacity style={styles.addItemButton} onPress={() => addItem()}>
<Ionicons name="add-circle" size={20} color={palette.purple[500]} />
<Text style={styles.addItemText}>{t('health.tabs.healthProfile.history.modal.addItem')}</Text>
</TouchableOpacity>
</View>
</View>
)}
</ScrollView>
{/* Save Button */}
<View style={styles.modalFooter}>
<TouchableOpacity
style={[styles.saveButton, isSaving && styles.saveButtonDisabled]}
onPress={handleSave}
disabled={isSaving}
>
<LinearGradient
colors={isSaving ? [palette.gray[300], palette.gray[400]] : [palette.purple[500], palette.purple[700]]}
style={styles.saveButtonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
{isSaving ? (
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<Text style={styles.saveButtonText}>{t('health.tabs.healthProfile.history.modal.save')}</Text>
)}
</LinearGradient>
</TouchableOpacity>
</View>
</View>
<DateTimePickerModal
isVisible={isDatePickerVisible}
mode="date"
onConfirm={handleConfirmDate}
onCancel={hideDatePicker}
maximumDate={new Date()} // Cannot select future date for history
confirmTextIOS={t('health.tabs.healthProfile.history.modal.save')} // Reuse save
cancelTextIOS={t('health.tabs.healthProfile.history.modal.none') === 'None' ? 'Cancel' : '取消'} // Fallback
/>
</KeyboardAvoidingView>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginBottom: 16,
position: 'relative',
},
glowContainer: {
position: 'absolute',
top: 20,
left: 20,
right: 20,
bottom: 20,
alignItems: 'center',
justifyContent: 'center',
zIndex: -1,
},
glow: {
width: '90%',
height: '90%',
backgroundColor: palette.purple[200],
opacity: 0.3,
borderRadius: 40,
transform: [{ scale: 1.05 }],
shadowColor: palette.purple[500],
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.4,
shadowRadius: 20,
},
card: {
backgroundColor: '#FFFFFF',
borderRadius: 24,
padding: 20,
shadowColor: palette.purple[100],
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.6,
shadowRadius: 24,
elevation: 4,
borderWidth: 1,
borderColor: '#F5F3FF',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
paddingHorizontal: 4,
},
headerTitle: {
fontSize: 18,
fontFamily: 'AliBold',
color: palette.gray[900],
fontWeight: '600',
},
list: {
backgroundColor: '#FFFFFF',
},
itemContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 4,
},
itemContainerWithList: {
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'flex-start',
},
itemHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
},
itemLeft: {
flexDirection: 'row',
alignItems: 'center',
},
indicator: {
width: 4,
height: 14,
borderRadius: 2,
marginRight: 12,
},
itemTitle: {
fontSize: 16,
color: palette.gray[700],
fontFamily: 'AliRegular',
},
itemStatus: {
fontSize: 14,
color: palette.gray[300],
fontFamily: 'AliRegular',
textAlign: 'right',
maxWidth: 150,
},
itemStatusActive: {
color: palette.purple[600],
fontWeight: '500',
},
subListContainer: {
marginTop: 12,
paddingLeft: 16, // Align with title (4px indicator + 12px margin)
},
subItemRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 6,
},
subItemDot: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: palette.purple[300],
marginRight: 8,
},
subItemName: {
flex: 1,
fontSize: 15,
color: palette.gray[800],
fontFamily: 'AliRegular',
fontWeight: '500',
},
subItemDate: {
fontSize: 13,
color: palette.gray[400],
fontFamily: 'AliRegular',
},
// 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,
maxHeight: '85%', // Increased height
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.2,
shadowRadius: 20,
elevation: 10,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
modalTitle: {
fontSize: 20,
fontFamily: 'AliBold',
color: palette.gray[900],
fontWeight: '600',
},
closeButton: {
padding: 4,
},
questionText: {
fontSize: 16,
color: palette.gray[700],
marginBottom: 12,
fontFamily: 'AliRegular',
},
radioGroup: {
flexDirection: 'row',
marginBottom: 24,
},
radioButton: {
flex: 1,
paddingVertical: 12,
borderWidth: 1,
borderColor: palette.gray[200],
borderRadius: 12,
alignItems: 'center',
marginRight: 8,
},
radioButtonActive: {
backgroundColor: palette.purple[50],
borderColor: palette.purple[500],
},
radioText: {
fontSize: 16,
color: palette.gray[600],
fontWeight: '500',
},
radioTextActive: {
color: palette.purple[600],
fontWeight: '600',
},
detailsContainer: {
marginTop: 4,
},
sectionLabel: {
fontSize: 14,
color: palette.gray[500],
marginBottom: 12,
marginTop: 8,
fontFamily: 'AliRegular',
},
recommendationContainer: {
marginBottom: 20,
},
tagsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
},
tag: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: '#F5F7FA',
},
tagText: {
fontSize: 14,
color: palette.gray[600],
fontFamily: 'AliRegular',
},
listContainer: {
marginTop: 8,
},
listItemCard: {
backgroundColor: '#F9FAFB',
borderRadius: 16,
padding: 16,
marginBottom: 12,
borderWidth: 1,
borderColor: palette.gray[100],
},
listItemHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
listItemNameInput: {
flex: 1,
fontSize: 16,
fontWeight: '600',
color: palette.gray[900],
fontFamily: 'AliBold',
padding: 0,
},
deleteButton: {
padding: 4,
},
datePickerTrigger: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF',
paddingHorizontal: 12,
paddingVertical: 10,
borderRadius: 12,
borderWidth: 1,
borderColor: palette.gray[200],
},
dateText: {
marginLeft: 8,
fontSize: 14,
color: palette.gray[900],
fontFamily: 'AliRegular',
},
placeholderText: {
color: palette.gray[400],
},
addItemButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
borderWidth: 1,
borderColor: palette.purple[200],
borderRadius: 12,
borderStyle: 'dashed',
backgroundColor: palette.purple[25],
marginTop: 4,
marginBottom: 20,
},
addItemText: {
marginLeft: 8,
fontSize: 14,
color: palette.purple[600],
fontWeight: '500',
},
modalFooter: {
marginTop: 8,
},
saveButton: {
borderRadius: 16,
overflow: 'hidden',
shadowColor: palette.purple[500],
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
saveButtonDisabled: {
shadowOpacity: 0,
},
saveButtonGradient: {
paddingVertical: 14,
alignItems: 'center',
},
saveButtonText: {
fontSize: 16,
color: '#FFFFFF',
fontWeight: '600',
fontFamily: 'AliBold',
},
});

View File

@@ -0,0 +1,49 @@
import { Ionicons } from '@expo/vector-icons';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export function MedicalRecordsTab() {
return (
<View style={styles.card}>
<View style={styles.emptyState}>
<Ionicons name="folder-open-outline" size={48} color="#E5E7EB" />
<Text style={styles.emptyText}></Text>
<Text style={styles.emptySubtext}></Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#FFFFFF',
borderRadius: 20,
padding: 40,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.03,
shadowRadius: 6,
elevation: 1,
minHeight: 200,
alignItems: 'center',
justifyContent: 'center',
},
emptyState: {
alignItems: 'center',
justifyContent: 'center',
},
emptyText: {
marginTop: 16,
fontSize: 16,
fontWeight: '600',
color: '#374151',
fontFamily: 'AliBold',
},
emptySubtext: {
marginTop: 8,
fontSize: 13,
color: '#9CA3AF',
fontFamily: 'AliRegular',
},
});