feat(用药管理): 集成AI智能分析功能,提供用药依从性深度洞察和专业健康建议

This commit is contained in:
richarjiang
2025-12-01 10:49:35 +08:00
parent a309123b35
commit a47f0fb72e
15 changed files with 1792 additions and 468 deletions

View File

@@ -5,6 +5,7 @@ import { TakenMedicationsStack } from '@/components/medication/TakenMedicationsS
import { ThemedText } from '@/components/ThemedText';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { MedicalDisclaimerSheet } from '@/components/ui/MedicalDisclaimerSheet';
import { MedicationAiSummaryInfoSheet } from '@/components/ui/MedicationAiSummaryInfoSheet';
import { Colors } from '@/constants/Colors';
import { useMembershipModal } from '@/contexts/MembershipModalContext';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
@@ -59,6 +60,7 @@ export default function MedicationsScreen() {
const [isCelebrationVisible, setIsCelebrationVisible] = useState(false);
const [disclaimerVisible, setDisclaimerVisible] = useState(false);
const [pendingAction, setPendingAction] = useState<'manual' | null>(null);
const [aiSummaryInfoVisible, setAiSummaryInfoVisible] = useState(false);
// 从 Redux 获取数据
const selectedKey = selectedDate.format('YYYY-MM-DD');
@@ -115,6 +117,33 @@ export default function MedicationsScreen() {
setPendingAction(null);
}, []);
const handleOpenAiSummary = useCallback(async () => {
// 先检查登录状态
const isLoggedIn = await ensureLoggedIn();
if (!isLoggedIn) return;
// 检查 VIP 权限
const access = checkServiceAccess();
if (!access.canUseService) {
// 非会员显示介绍弹窗
setAiSummaryInfoVisible(true);
return;
}
// 会员直接跳转到 AI 总结页面
router.push('/medications/ai-summary');
}, [checkServiceAccess, ensureLoggedIn]);
const handleAiSummaryInfoConfirm = useCallback(() => {
setAiSummaryInfoVisible(false);
// 点击"我要订阅"后,弹出会员订阅弹窗
openMembershipModal();
}, [openMembershipModal]);
const handleAiSummaryInfoClose = useCallback(() => {
setAiSummaryInfoVisible(false);
}, []);
const handleOpenMedicationManagement = useCallback(() => {
router.push('/medications/manage-medications');
}, []);
@@ -285,31 +314,59 @@ export default function MedicationsScreen() {
</ThemedText>
</View>
<View style={styles.headerActions}>
<TouchableOpacity
activeOpacity={0.7}
onPress={handleOpenMedicationManagement}
>
{isLiquidGlassAvailable() ? (
{isLiquidGlassAvailable() ? (
<TouchableOpacity
activeOpacity={0.7}
onPress={handleOpenAiSummary}
>
<GlassView
style={styles.headerAddButton}
glassEffectStyle="clear"
tintColor="rgba(255, 255, 255, 0.3)"
tintColor="rgba(255, 255, 255, 0.36)"
isInteractive={true}
>
<IconSymbol name="pills.fill" size={18} color="#333" />
<IconSymbol name="sparkles" size={18} color="#333" />
</GlassView>
) : (
<View style={[styles.headerAddButton, styles.fallbackAddButton]}>
<IconSymbol name="pills.fill" size={18} color="#333" />
</View>
)}
</TouchableOpacity>
</TouchableOpacity>
) : (
<TouchableOpacity
activeOpacity={0.7}
onPress={handleOpenAiSummary}
style={[styles.headerAddButton, styles.fallbackAddButton]}
>
<IconSymbol name="sparkles" size={18} color="#333" />
</TouchableOpacity>
)}
<TouchableOpacity
activeOpacity={0.7}
onPress={handleAddMedication}
>
{isLiquidGlassAvailable() ? (
{isLiquidGlassAvailable() ? (
<TouchableOpacity
activeOpacity={0.7}
onPress={handleOpenMedicationManagement}
>
<GlassView
style={styles.headerAddButton}
glassEffectStyle="clear"
tintColor="rgba(255, 255, 255, 0.3)"
isInteractive={true}
>
<IconSymbol name="pills.fill" size={18} color="#333" />
</GlassView>
</TouchableOpacity>
) : (
<TouchableOpacity
activeOpacity={0.7}
onPress={handleOpenMedicationManagement}
style={[styles.headerAddButton, styles.fallbackAddButton]}
>
<IconSymbol name="pills.fill" size={18} color="#333" />
</TouchableOpacity>
)}
{isLiquidGlassAvailable() ? (
<TouchableOpacity
activeOpacity={0.7}
onPress={handleAddMedication}
>
<GlassView
style={styles.headerAddButton}
glassEffectStyle="clear"
@@ -318,12 +375,16 @@ export default function MedicationsScreen() {
>
<IconSymbol name="plus" size={18} color="#333" />
</GlassView>
) : (
<View style={[styles.headerAddButton, styles.fallbackAddButton]}>
<IconSymbol name="plus" size={18} color="#333" />
</View>
)}
</TouchableOpacity>
</TouchableOpacity>
) : (
<TouchableOpacity
activeOpacity={0.7}
onPress={handleAddMedication}
style={[styles.headerAddButton, styles.fallbackAddButton]}
>
<IconSymbol name="plus" size={18} color="#333" />
</TouchableOpacity>
)}
</View>
</View>
@@ -430,6 +491,13 @@ export default function MedicationsScreen() {
onClose={handleDisclaimerClose}
onConfirm={handleDisclaimerConfirm}
/>
{/* AI 用药总结介绍弹窗 */}
<MedicationAiSummaryInfoSheet
visible={aiSummaryInfoVisible}
onClose={handleAiSummaryInfoClose}
onConfirm={handleAiSummaryInfoConfirm}
/>
</View>
);
}

View File

@@ -0,0 +1,886 @@
import { ThemedText } from '@/components/ThemedText';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { getMedicationAiSummary } from '@/services/medications';
import { type MedicationAiSummary, type MedicationAiSummaryItem } from '@/types/medication';
import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Modal,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function MedicationAiSummaryScreen() {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const [summary, setSummary] = useState<MedicationAiSummary | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<string>('');
const [showInfoModal, setShowInfoModal] = useState(false);
const [showCompletionInfoModal, setShowCompletionInfoModal] = useState(false);
const fetchSummary = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await getMedicationAiSummary();
setSummary(data);
setLastUpdated(dayjs().format('YYYY.MM.DD HH:mm'));
} catch (err: any) {
const status = err?.status;
if (status === 403) {
setError(t('medications.aiSummary.error403'));
} else {
setError(err?.message || t('medications.aiSummary.genericError'));
}
setSummary(null);
} finally {
setLoading(false);
}
}, [t]);
useFocusEffect(
useCallback(() => {
fetchSummary();
}, [fetchSummary])
);
const handleExplainRefresh = useCallback(() => {
setShowInfoModal(true);
}, []);
const handleExplainCompletion = useCallback(() => {
setShowCompletionInfoModal(true);
}, []);
const medicationItems = summary?.medicationAnalysis ?? [];
const isEmpty = !loading && !error && medicationItems.length === 0;
const stats = useMemo(() => {
const plannedDoses = medicationItems.reduce((acc, item) => acc + (item.plannedDoses || 0), 0);
const takenDoses = medicationItems.reduce((acc, item) => acc + (item.takenDoses || 0), 0);
const completion = plannedDoses > 0 ? takenDoses / plannedDoses : 0;
const avgCompletion =
medicationItems.length > 0
? medicationItems.reduce((acc, item) => acc + (item.completionRate || 0), 0) /
medicationItems.length
: 0;
const plannedDays = medicationItems.reduce((acc, item) => acc + (item.plannedDays || 0), 0);
return {
plannedDoses,
takenDoses,
completion,
avgCompletion,
plannedDays,
activePlans: medicationItems.length,
};
}, [medicationItems]);
const completionPercent = Math.min(100, Math.round(stats.completion * 100));
const renderMedicationCard = (item: MedicationAiSummaryItem) => {
const percent = Math.min(100, Math.round((item.completionRate || 0) * 100));
return (
<View key={item.id} style={styles.planCard}>
<View style={styles.planHeader}>
<View style={{ flex: 1 }}>
<ThemedText style={styles.planName}>{item.name}</ThemedText>
<ThemedText style={styles.planMeta}>
{t('medications.aiSummary.daysLabel', {
days: item.plannedDays,
times: item.timesPerDay,
})}
</ThemedText>
</View>
<View style={styles.planChip}>
<IconSymbol name="sparkles" size={14} color="#d6b37f" />
<ThemedText style={styles.planChipText}>
{t('medications.aiSummary.badges.adherence')}
</ThemedText>
</View>
</View>
<View style={styles.progressRow}>
<View style={styles.progressTrack}>
<View style={[styles.progressFill, { width: `${percent}%` }]} />
</View>
<ThemedText style={styles.progressValue}>
{t('medications.aiSummary.completionLabel', { value: percent })}
</ThemedText>
</View>
<View style={styles.planFooter}>
<ThemedText style={styles.planStat}>
{t('medications.aiSummary.doseSummary', {
taken: item.takenDoses,
planned: item.plannedDoses,
})}
</ThemedText>
<ThemedText style={styles.planDate}>
{dayjs(item.startDate).format('YYYY.MM.DD')}
</ThemedText>
</View>
</View>
);
};
const headerTitle = (
<View style={styles.headerTitle}>
<ThemedText style={styles.title}>{t('medications.aiSummary.title')}</ThemedText>
<ThemedText style={styles.subtitle}>{t('medications.aiSummary.subtitle')}</ThemedText>
</View>
);
return (
<View style={styles.container}>
<LinearGradient
colors={['#0a0e16', '#0b101a', '#0b0f16']}
style={StyleSheet.absoluteFill}
/>
<View style={styles.glowTop} />
<View style={styles.glowBottom} />
<HeaderBar
title={headerTitle}
tone="dark"
transparent
variant="minimal"
right={
<TouchableOpacity
style={styles.iconButton}
onPress={handleExplainRefresh}
activeOpacity={0.8}
>
<IconSymbol name="info.circle" size={20} color="#dfe8ff" />
</TouchableOpacity>
}
/>
<ScrollView
contentContainerStyle={[
styles.scrollContent,
{ paddingBottom: insets.bottom + 32, paddingTop: insets.top + 80 },
]}
showsVerticalScrollIndicator={false}
>
<LinearGradient
colors={['#131a28', '#0f1623']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.heroCard}
>
<View style={styles.heroHeader}>
<ThemedText style={styles.heroLabel}>
{t('medications.aiSummary.overviewTitle')}
</ThemedText>
<ThemedText style={styles.updatedAt}>
{lastUpdated ? t('medications.aiSummary.updatedAt', { time: lastUpdated }) : ' '}
</ThemedText>
</View>
<View style={styles.heroMainRow}>
<View style={styles.heroLeft}>
<ThemedText style={styles.heroValue}>{completionPercent}%</ThemedText>
<ThemedText style={styles.heroCaption}>
{t('medications.aiSummary.doseSummary', {
taken: stats.takenDoses,
planned: stats.plannedDoses,
})}
</ThemedText>
<View style={styles.heroProgressTrack}>
<View style={[styles.heroProgressFill, { width: `${completionPercent}%` }]} />
</View>
</View>
<View style={styles.heroChip}>
<ThemedText style={styles.heroChipLabel}>
{t('medications.aiSummary.badges.safety')}
</ThemedText>
<ThemedText style={styles.heroChipValue}>{stats.activePlans}</ThemedText>
<ThemedText style={styles.heroChipHint}>
{t('medications.aiSummary.stats.activePlans')}
</ThemedText>
</View>
</View>
<View style={styles.heroStatsRow}>
<View style={styles.heroStatItem}>
<ThemedText style={styles.heroStatLabel}>
{t('medications.aiSummary.stats.avgCompletion')}
</ThemedText>
<ThemedText style={styles.heroStatValue}>
{Math.round(stats.avgCompletion * 100)}%
</ThemedText>
</View>
<View style={styles.heroStatItem}>
<ThemedText style={styles.heroStatLabel}>
{t('medications.aiSummary.stats.activeDays')}
</ThemedText>
<ThemedText style={styles.heroStatValue}>{stats.plannedDays}</ThemedText>
</View>
<View style={styles.heroStatItem}>
<ThemedText style={styles.heroStatLabel}>
{t('medications.aiSummary.stats.takenDoses')}
</ThemedText>
<ThemedText style={styles.heroStatValue}>{stats.takenDoses}</ThemedText>
</View>
</View>
</LinearGradient>
{error ? (
<View style={styles.errorCard}>
<ThemedText style={styles.errorTitle}>{error}</ThemedText>
<TouchableOpacity style={styles.retryButton} onPress={fetchSummary} activeOpacity={0.85}>
<ThemedText style={styles.retryText}>{t('medications.aiSummary.retry')}</ThemedText>
</TouchableOpacity>
</View>
) : (
<>
<View style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<ThemedText style={styles.sectionTitle}>
{t('medications.aiSummary.keyInsights')}
</ThemedText>
<View style={styles.pillChip}>
<IconSymbol name="sparkles" size={14} color="#0b0f16" />
<ThemedText style={styles.pillChipText}>
{t('medications.aiSummary.pillChip')}
</ThemedText>
</View>
</View>
<ThemedText style={styles.insightText}>
{summary?.keyInsights || t('medications.aiSummary.keyInsightPlaceholder')}
</ThemedText>
</View>
<View style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<ThemedText style={styles.sectionTitle}>
{t('medications.aiSummary.listTitle')}
</ThemedText>
<TouchableOpacity
style={styles.infoIconButton}
onPress={handleExplainCompletion}
activeOpacity={0.8}
>
<IconSymbol name="info.circle" size={16} color="#8b94a8" />
</TouchableOpacity>
</View>
{loading ? (
<View style={styles.loadingRow}>
<ActivityIndicator color="#d6b37f" />
<ThemedText style={styles.loadingText}>
{t('medications.aiSummary.refresh')}
</ThemedText>
</View>
) : isEmpty ? (
<View style={styles.emptyState}>
<ThemedText style={styles.emptyTitle}>
{t('medications.aiSummary.emptyTitle')}
</ThemedText>
<ThemedText style={styles.emptySubtitle}>
{t('medications.aiSummary.emptyDescription')}
</ThemedText>
</View>
) : (
<View style={styles.planList}>{medicationItems.map(renderMedicationCard)}</View>
)}
</View>
</>
)}
</ScrollView>
<Modal
visible={showInfoModal}
transparent
animationType="fade"
onRequestClose={() => setShowInfoModal(false)}
>
<TouchableOpacity
style={styles.infoOverlay}
activeOpacity={1}
onPress={() => setShowInfoModal(false)}
>
<TouchableOpacity
activeOpacity={1}
onPress={(e) => e.stopPropagation()}
style={styles.infoModal}
>
<LinearGradient
colors={['#111827', '#0b1220']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.infoGradient}
>
<View style={styles.infoHeader}>
<ThemedText style={styles.infoBadge}>{t('medications.aiSummary.infoModal.badge')}</ThemedText>
<ThemedText style={styles.infoTitle}>{t('medications.aiSummary.infoModal.title')}</ThemedText>
<TouchableOpacity
onPress={() => setShowInfoModal(false)}
style={styles.infoClose}
accessibilityLabel="close"
>
<IconSymbol name="xmark" size={18} color="#e5e7eb" />
</TouchableOpacity>
</View>
<View style={styles.infoContent}>
<Text style={styles.infoText}>
{t('medications.aiSummary.infoModal.point1')}
</Text>
<Text style={styles.infoText}>
{t('medications.aiSummary.infoModal.point2')}
</Text>
<Text style={styles.infoText}>
{t('medications.aiSummary.infoModal.point3')}
</Text>
<Text style={styles.infoText}>
{t('medications.aiSummary.infoModal.point4')}
</Text>
</View>
<View style={styles.infoButtonContainer}>
<TouchableOpacity
onPress={() => setShowInfoModal(false)}
>
<LinearGradient
colors={['#d6b37f', '#c59b63']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.infoButton}
>
<Text style={styles.infoButtonText}>{t('medications.aiSummary.infoModal.button')}</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</LinearGradient>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
<Modal
visible={showCompletionInfoModal}
transparent
animationType="fade"
onRequestClose={() => setShowCompletionInfoModal(false)}
>
<TouchableOpacity
style={styles.infoOverlay}
activeOpacity={1}
onPress={() => setShowCompletionInfoModal(false)}
>
<TouchableOpacity
activeOpacity={1}
onPress={(e) => e.stopPropagation()}
style={styles.infoModal}
>
<LinearGradient
colors={['#111827', '#0b1220']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.infoGradient}
>
<View style={styles.infoHeader}>
<ThemedText style={styles.infoBadge}>{t('medications.aiSummary.completionInfoModal.badge')}</ThemedText>
<ThemedText style={styles.infoTitle}>{t('medications.aiSummary.completionInfoModal.title')}</ThemedText>
<TouchableOpacity
onPress={() => setShowCompletionInfoModal(false)}
style={styles.infoClose}
accessibilityLabel="close"
>
<IconSymbol name="xmark" size={18} color="#e5e7eb" />
</TouchableOpacity>
</View>
<View style={styles.infoContent}>
<Text style={styles.infoText}>
{t('medications.aiSummary.completionInfoModal.point1')}
</Text>
<Text style={styles.infoText}>
{t('medications.aiSummary.completionInfoModal.point2')}
</Text>
<Text style={styles.infoText}>
{t('medications.aiSummary.completionInfoModal.point3')}
</Text>
<Text style={styles.infoText}>
{t('medications.aiSummary.completionInfoModal.point4')}
</Text>
<Text style={styles.infoText}>
{t('medications.aiSummary.completionInfoModal.point5')}
</Text>
</View>
<View style={styles.infoButtonContainer}>
<TouchableOpacity
onPress={() => setShowCompletionInfoModal(false)}
>
<LinearGradient
colors={['#d6b37f', '#c59b63']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.infoButton}
>
<Text style={styles.infoButtonText}>{t('medications.aiSummary.completionInfoModal.button')}</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</LinearGradient>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0b0f16',
},
scrollContent: {
paddingHorizontal: 20,
gap: 20,
},
glowTop: {
position: 'absolute',
top: -80,
left: -40,
width: 200,
height: 200,
backgroundColor: '#1b2a44',
opacity: 0.35,
borderRadius: 140,
},
glowBottom: {
position: 'absolute',
bottom: -120,
right: -60,
width: 240,
height: 240,
backgroundColor: '#123125',
opacity: 0.25,
borderRadius: 200,
},
iconButton: {
width: 40,
height: 40,
borderRadius: 14,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.08)',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(255,255,255,0.04)',
},
headerTitle: {
alignItems: 'center',
flex: 1,
gap: 6,
},
badge: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 999,
backgroundColor: '#d6b37f',
},
badgeText: {
color: '#0b0f16',
fontSize: 12,
fontWeight: '700',
fontFamily: 'AliBold',
},
title: {
color: '#f6f7fb',
fontSize: 22,
fontFamily: 'AliBold',
},
subtitle: {
color: '#b9c2d3',
fontSize: 14,
fontFamily: 'AliRegular',
},
heroCard: {
borderRadius: 24,
padding: 18,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.06)',
shadowColor: '#000',
shadowOpacity: 0.25,
shadowRadius: 16,
gap: 14,
},
heroHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
heroLabel: {
color: '#f5f6fb',
fontSize: 16,
fontFamily: 'AliBold',
},
updatedAt: {
color: '#8b94a8',
fontSize: 12,
fontFamily: 'AliRegular',
},
heroMainRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 16,
},
heroLeft: {
flex: 1,
minWidth: 0,
},
heroValue: {
color: '#36d0a5',
fontSize: 38,
lineHeight: 42,
fontFamily: 'AliBold',
letterSpacing: 0.5,
flexShrink: 1,
},
heroCaption: {
color: '#c2ccdf',
fontSize: 13,
fontFamily: 'AliRegular',
marginTop: 4,
},
heroProgressTrack: {
marginTop: 12,
height: 10,
borderRadius: 10,
backgroundColor: 'rgba(255,255,255,0.08)',
overflow: 'hidden',
},
heroProgressFill: {
height: '100%',
borderRadius: 10,
backgroundColor: '#36d0a5',
},
heroChip: {
paddingHorizontal: 14,
paddingVertical: 12,
borderRadius: 18,
backgroundColor: 'rgba(214, 179, 127, 0.12)',
borderWidth: 1,
borderColor: 'rgba(214, 179, 127, 0.3)',
minWidth: 120,
alignItems: 'flex-start',
gap: 4,
},
heroChipLabel: {
color: '#d6b37f',
fontSize: 12,
fontFamily: 'AliRegular',
},
heroChipValue: {
color: '#f6f7fb',
fontSize: 20,
fontFamily: 'AliBold',
lineHeight: 24,
},
heroChipHint: {
color: '#b9c2d3',
fontSize: 12,
fontFamily: 'AliRegular',
},
heroStatsRow: {
flexDirection: 'row',
gap: 12,
justifyContent: 'space-between',
},
heroStatItem: {
flex: 1,
padding: 12,
borderRadius: 14,
backgroundColor: 'rgba(255,255,255,0.04)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.04)',
},
heroStatLabel: {
color: '#9dabc4',
fontSize: 12,
fontFamily: 'AliRegular',
},
heroStatValue: {
color: '#f6f7fb',
fontSize: 18,
marginTop: 6,
fontFamily: 'AliBold',
},
sectionCard: {
borderRadius: 20,
padding: 16,
backgroundColor: 'rgba(255,255,255,0.03)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.05)',
gap: 12,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
sectionTitle: {
color: '#f5f6fb',
fontSize: 16,
fontFamily: 'AliBold',
},
pillChip: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
backgroundColor: '#d6b37f',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 999,
},
pillChipText: {
color: '#0b0f16',
fontSize: 12,
fontFamily: 'AliBold',
},
insightText: {
color: '#d9e2f2',
fontSize: 15,
lineHeight: 22,
fontFamily: 'AliRegular',
},
planList: {
gap: 12,
},
planCard: {
borderRadius: 16,
padding: 14,
backgroundColor: 'rgba(255,255,255,0.04)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.06)',
gap: 10,
},
planHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
planName: {
color: '#f6f7fb',
fontSize: 16,
fontFamily: 'AliBold',
},
planMeta: {
color: '#9dabc4',
fontSize: 12,
fontFamily: 'AliRegular',
marginTop: 2,
},
planChip: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 999,
backgroundColor: 'rgba(214, 179, 127, 0.15)',
borderWidth: 1,
borderColor: 'rgba(214, 179, 127, 0.35)',
},
planChipText: {
color: '#d6b37f',
fontSize: 12,
fontFamily: 'AliBold',
},
progressRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
progressTrack: {
flex: 1,
height: 10,
borderRadius: 10,
backgroundColor: 'rgba(255,255,255,0.08)',
overflow: 'hidden',
},
progressFill: {
height: '100%',
backgroundColor: '#36d0a5',
borderRadius: 10,
},
progressValue: {
color: '#f6f7fb',
fontSize: 12,
fontFamily: 'AliBold',
},
planFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
planStat: {
color: '#c7d1e4',
fontSize: 13,
fontFamily: 'AliRegular',
},
planDate: {
color: '#7f8aa4',
fontSize: 12,
fontFamily: 'AliRegular',
},
errorCard: {
padding: 16,
borderRadius: 16,
backgroundColor: 'rgba(255, 86, 86, 0.08)',
borderWidth: 1,
borderColor: 'rgba(255, 86, 86, 0.3)',
alignItems: 'center',
gap: 12,
},
errorTitle: {
color: '#ff9c9c',
fontSize: 14,
textAlign: 'center',
fontFamily: 'AliBold',
},
retryButton: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 999,
backgroundColor: '#ff9c9c',
},
retryText: {
color: '#0b0f16',
fontSize: 13,
fontFamily: 'AliBold',
},
loadingRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
paddingVertical: 12,
},
loadingText: {
color: '#c7d1e4',
fontSize: 13,
fontFamily: 'AliRegular',
},
emptyState: {
paddingVertical: 12,
gap: 6,
},
emptyTitle: {
color: '#f6f7fb',
fontSize: 15,
fontFamily: 'AliBold',
},
emptySubtitle: {
color: '#9dabc4',
fontSize: 13,
fontFamily: 'AliRegular',
lineHeight: 20,
},
infoOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.6)',
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 20,
},
infoModal: {
width: '100%',
maxWidth: 400,
borderRadius: 24,
overflow: 'hidden',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
},
infoGradient: {
padding: 24,
gap: 20,
},
infoHeader: {
alignItems: 'center',
justifyContent: 'center',
marginBottom: 4,
},
infoBadge: {
color: '#d6b37f',
fontSize: 24,
lineHeight: 28,
fontFamily: 'AliBold',
marginBottom: 10,
letterSpacing: 0.5,
},
infoTitle: {
color: '#f6f7fb',
fontSize: 16,
fontFamily: 'AliBold',
textAlign: 'center',
},
infoClose: {
position: 'absolute',
right: -4,
top: -4,
padding: 8,
width: 36,
height: 36,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 18,
backgroundColor: 'rgba(255,255,255,0.05)',
},
infoContent: {
gap: 14,
},
infoText: {
color: '#d9e2f2',
fontSize: 14,
lineHeight: 18,
fontFamily: 'AliRegular',
},
infoButtonContainer: {
marginTop: 12,
alignItems: 'center',
},
infoButtonWrapper: {
// minWidth: 120,
// maxWidth: 180,
},
infoButton: {
borderRadius: 12,
paddingVertical: 10,
paddingHorizontal: 28,
alignItems: 'center',
overflow: 'hidden',
},
infoButtonGlass: {
paddingVertical: 10,
paddingHorizontal: 28,
alignItems: 'center',
},
infoButtonText: {
color: '#0b0f16',
fontSize: 15,
fontFamily: 'AliBold',
letterSpacing: 0.2,
},
infoIconButton: {
width: 28,
height: 28,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(139, 148, 168, 0.1)',
},
});