feat(用药管理): 集成AI智能分析功能,提供用药依从性深度洞察和专业健康建议
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
886
app/medications/ai-summary.tsx
Normal file
886
app/medications/ai-summary.tsx
Normal 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)',
|
||||
},
|
||||
});
|
||||
BIN
assets/images/medicine/medicine-ai-summary.png
Normal file
BIN
assets/images/medicine/medicine-ai-summary.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
@@ -1,14 +1,17 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
InteractionManager,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle
|
||||
Animated,
|
||||
InteractionManager,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { ChallengeType } from '@/services/challengesApi';
|
||||
import { reportChallengeProgress, selectChallengeList } from '@/store/challengesSlice';
|
||||
import { fetchHourlyStepSamples, fetchStepCount, HourlyStepData } from '@/utils/health';
|
||||
import { logger } from '@/utils/logger';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -20,8 +23,8 @@ import { AnimatedNumber } from './AnimatedNumber';
|
||||
// import Svg, { Rect } from 'react-native-svg';
|
||||
|
||||
interface StepsCardProps {
|
||||
curDate: Date
|
||||
stepGoal: number;
|
||||
curDate: Date;
|
||||
stepGoal?: number;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
@@ -31,9 +34,20 @@ const StepsCard: React.FC<StepsCardProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const challenges = useAppSelector(selectChallengeList);
|
||||
|
||||
const [stepCount, setStepCount] = useState(0)
|
||||
const [hourlySteps, setHourSteps] = useState<HourlyStepData[]>([])
|
||||
const [stepCount, setStepCount] = useState(0);
|
||||
const [hourlySteps, setHourSteps] = useState<HourlyStepData[]>([]);
|
||||
|
||||
// 过滤出已参加的步数挑战
|
||||
const joinedStepsChallenges = useMemo(
|
||||
() => challenges.filter((challenge) => challenge.type === ChallengeType.STEP && challenge.isJoined && challenge.status === 'ongoing'),
|
||||
[challenges]
|
||||
);
|
||||
|
||||
// 跟踪上次上报的记录,避免重复上报
|
||||
const lastReportedRef = useRef<{ date: string; value: number } | null>(null);
|
||||
|
||||
|
||||
const getStepData = useCallback(async (date: Date) => {
|
||||
@@ -59,6 +73,42 @@ const StepsCard: React.FC<StepsCardProps> = ({
|
||||
}
|
||||
}, [curDate]);
|
||||
|
||||
// 步数挑战进度上报逻辑
|
||||
useEffect(() => {
|
||||
if (!curDate || !stepCount || !joinedStepsChallenges.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果当前日期不是今天,不上报
|
||||
if (!dayjs(curDate).isSame(dayjs(), 'day')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateKey = dayjs(curDate).format('YYYY-MM-DD');
|
||||
const lastReport = lastReportedRef.current;
|
||||
|
||||
if (lastReport && lastReport.date === dateKey && lastReport.value === stepCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reportProgress = async () => {
|
||||
const stepsChallenge = joinedStepsChallenges.find((c) => c.type === ChallengeType.STEP);
|
||||
if (!stepsChallenge) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await dispatch(reportChallengeProgress({ id: stepsChallenge.id, value: stepCount })).unwrap();
|
||||
} catch (error) {
|
||||
logger.warn('StepsCard: Challenge progress report failed', { error, challengeId: stepsChallenge.id });
|
||||
}
|
||||
|
||||
lastReportedRef.current = { date: dateKey, value: stepCount };
|
||||
};
|
||||
|
||||
reportProgress();
|
||||
}, [dispatch, joinedStepsChallenges, curDate, stepCount]);
|
||||
|
||||
// 优化:减少动画值数量,只为有数据的小时创建动画
|
||||
const animatedValues = useRef<Map<number, Animated.Value>>(new Map()).current;
|
||||
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
InteractionManager
|
||||
} from 'react-native';
|
||||
|
||||
import { fetchHourlyStepSamples, fetchStepCount, HourlyStepData } from '@/utils/health';
|
||||
import { logger } from '@/utils/logger';
|
||||
import dayjs from 'dayjs';
|
||||
import { Image } from 'expo-image';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { AnimatedNumber } from './AnimatedNumber';
|
||||
|
||||
interface StepsCardProps {
|
||||
curDate: Date
|
||||
stepGoal: number;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
const StepsCardOptimized: React.FC<StepsCardProps> = ({
|
||||
curDate,
|
||||
style,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const [stepCount, setStepCount] = useState(0)
|
||||
const [hourlySteps, setHourSteps] = useState<HourlyStepData[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// 优化:使用debounce减少频繁的数据获取
|
||||
const debounceTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const getStepData = useCallback(async (date: Date) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
logger.info('获取步数数据...');
|
||||
|
||||
// 先获取步数,立即更新UI
|
||||
const steps = await fetchStepCount(date);
|
||||
setStepCount(steps);
|
||||
|
||||
// 清除之前的定时器
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current);
|
||||
}
|
||||
|
||||
// 使用 InteractionManager 在空闲时获取更复杂的小时数据
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
try {
|
||||
const hourly = await fetchHourlyStepSamples(date);
|
||||
setHourSteps(hourly);
|
||||
} catch (error) {
|
||||
logger.error('获取小时步数数据失败:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取步数数据失败:', error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (curDate) {
|
||||
getStepData(curDate);
|
||||
}
|
||||
}, [curDate, getStepData]);
|
||||
|
||||
// 优化:减少动画值数量,只为有数据的小时创建动画
|
||||
const animatedValues = useRef<Map<number, Animated.Value>>(new Map()).current;
|
||||
|
||||
// 优化:简化柱状图数据计算,减少计算量
|
||||
const chartData = useMemo(() => {
|
||||
if (!hourlySteps || hourlySteps.length === 0) {
|
||||
return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0, height: 0 }));
|
||||
}
|
||||
|
||||
// 优化:只计算有数据的小时的最大步数
|
||||
const activeSteps = hourlySteps.filter(data => data.steps > 0);
|
||||
if (activeSteps.length === 0) {
|
||||
return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0, height: 0 }));
|
||||
}
|
||||
|
||||
const maxSteps = Math.max(...activeSteps.map(data => data.steps));
|
||||
const maxHeight = 20;
|
||||
|
||||
return hourlySteps.map(data => ({
|
||||
...data,
|
||||
height: data.steps > 0 ? (data.steps / maxSteps) * maxHeight : 0
|
||||
}));
|
||||
}, [hourlySteps]);
|
||||
|
||||
// 获取当前小时
|
||||
const currentHour = new Date().getHours();
|
||||
|
||||
// 优化:延迟执行动画,减少UI阻塞
|
||||
useEffect(() => {
|
||||
const hasData = chartData && chartData.length > 0 && chartData.some(data => data.steps > 0);
|
||||
|
||||
if (hasData && !isLoading) {
|
||||
// 使用 InteractionManager 确保动画不会阻塞用户交互
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
// 只为有数据的小时创建和执行动画
|
||||
const animations = chartData
|
||||
.map((data, index) => {
|
||||
if (data.steps > 0) {
|
||||
// 懒创建动画值
|
||||
if (!animatedValues.has(index)) {
|
||||
animatedValues.set(index, new Animated.Value(0));
|
||||
}
|
||||
|
||||
const animValue = animatedValues.get(index)!;
|
||||
animValue.setValue(0);
|
||||
|
||||
// 使用更高性能的timing动画替代spring
|
||||
return Animated.timing(animValue, {
|
||||
toValue: 1,
|
||||
duration: 200, // 减少动画时长
|
||||
useNativeDriver: false,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as Animated.CompositeAnimation[];
|
||||
|
||||
// 批量执行动画,提高性能
|
||||
if (animations.length > 0) {
|
||||
Animated.stagger(50, animations).start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [chartData, animatedValues, isLoading]);
|
||||
|
||||
// 优化:使用React.memo包装复杂的渲染组件
|
||||
const ChartBars = useMemo(() => {
|
||||
return chartData.map((data, index) => {
|
||||
// 判断是否是当前小时或者有活动的小时
|
||||
const isActive = data.steps > 0;
|
||||
const isCurrent = index <= currentHour;
|
||||
|
||||
// 优化:只为有数据的柱体创建动画插值
|
||||
const animValue = animatedValues.get(index);
|
||||
let animatedScale: Animated.AnimatedInterpolation<number> | undefined;
|
||||
let animatedOpacity: Animated.AnimatedInterpolation<number> | undefined;
|
||||
|
||||
if (animValue && isActive) {
|
||||
animatedScale = animValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
});
|
||||
|
||||
animatedOpacity = animValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={`bar-container-${index}`} style={styles.barContainer}>
|
||||
{/* 背景柱体 - 始终显示,使用相似色系的淡色 */}
|
||||
<View
|
||||
style={[
|
||||
styles.chartBar,
|
||||
{
|
||||
height: 20, // 背景柱体占满整个高度
|
||||
backgroundColor: isCurrent ? '#FFF4E6' : '#FFF8F0', // 更淡的相似色系
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 数据柱体 - 只有当有数据时才显示并执行动画 */}
|
||||
{isActive && (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.chartBar,
|
||||
{
|
||||
height: data.height,
|
||||
backgroundColor: isCurrent ? '#FFC365' : '#FFEBCB',
|
||||
transform: animatedScale ? [{ scaleY: animatedScale }] : undefined,
|
||||
opacity: animatedOpacity || 1,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
}, [chartData, currentHour, animatedValues]);
|
||||
|
||||
const CardContent = () => (
|
||||
<>
|
||||
{/* 标题和步数显示 */}
|
||||
<View style={styles.header}>
|
||||
<Image
|
||||
source={require('@/assets/images/icons/icon-step.png')}
|
||||
style={styles.titleIcon}
|
||||
/>
|
||||
<Text style={styles.title}>步数</Text>
|
||||
{isLoading && <Text style={styles.loadingText}>加载中...</Text>}
|
||||
</View>
|
||||
|
||||
{/* 柱状图 */}
|
||||
<View style={styles.chartContainer}>
|
||||
<View style={styles.chartWrapper}>
|
||||
<View style={styles.chartArea}>
|
||||
{ChartBars}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 步数和目标显示 */}
|
||||
<View style={styles.statsContainer}>
|
||||
<AnimatedNumber
|
||||
value={stepCount || 0}
|
||||
style={styles.stepCount}
|
||||
format={(v) => stepCount !== null ? `${Math.round(v)}` : '——'}
|
||||
resetToken={stepCount}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.container, style]}
|
||||
onPress={() => {
|
||||
// 传递当前日期参数到详情页
|
||||
const dateParam = dayjs(curDate).format('YYYY-MM-DD');
|
||||
router.push(`/steps/detail?date=${dateParam}`);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<CardContent />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
borderRadius: 20,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 20,
|
||||
elevation: 8,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
},
|
||||
titleIcon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
marginRight: 6,
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
color: '#192126',
|
||||
fontWeight: '600'
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 10,
|
||||
color: '#666',
|
||||
marginLeft: 8,
|
||||
},
|
||||
chartContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
marginTop: 6
|
||||
},
|
||||
chartWrapper: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
chartArea: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
height: 20,
|
||||
width: '100%',
|
||||
maxWidth: 240,
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
barContainer: {
|
||||
width: 4,
|
||||
height: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
position: 'relative',
|
||||
},
|
||||
chartBar: {
|
||||
width: 4,
|
||||
borderRadius: 1,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
},
|
||||
statsContainer: {
|
||||
alignItems: 'flex-start',
|
||||
marginTop: 6
|
||||
},
|
||||
stepCount: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#192126',
|
||||
},
|
||||
});
|
||||
|
||||
export default StepsCardOptimized;
|
||||
@@ -30,6 +30,9 @@ const MAPPING = {
|
||||
'info.circle': 'info',
|
||||
'magnifyingglass': 'search',
|
||||
'xmark': 'close',
|
||||
'chevron.left': 'chevron-left',
|
||||
'sparkles': 'auto-awesome',
|
||||
'arrow.clockwise': 'refresh',
|
||||
} as IconMapping;
|
||||
|
||||
/**
|
||||
|
||||
454
components/ui/MedicationAiSummaryInfoSheet.tsx
Normal file
454
components/ui/MedicationAiSummaryInfoSheet.tsx
Normal file
@@ -0,0 +1,454 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { Image } from 'expo-image';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
BackHandler,
|
||||
Dimensions,
|
||||
Modal,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import ImageViewing from 'react-native-image-viewing';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useI18n } from '../../hooks/useI18n';
|
||||
import { triggerLightHaptic } from '../../utils/haptics';
|
||||
|
||||
const { height: screenHeight } = Dimensions.get('window');
|
||||
|
||||
interface MedicationAiSummaryInfoSheetProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 用药总结介绍弹窗组件
|
||||
* 用于展示 AI 用药总结功能的介绍和说明
|
||||
*/
|
||||
export function MedicationAiSummaryInfoSheet({
|
||||
visible,
|
||||
onClose,
|
||||
onConfirm,
|
||||
loading = false,
|
||||
}: MedicationAiSummaryInfoSheetProps) {
|
||||
const { t } = useI18n();
|
||||
const insets = useSafeAreaInsets();
|
||||
const translateY = useRef(new Animated.Value(screenHeight)).current;
|
||||
const backdropOpacity = useRef(new Animated.Value(0)).current;
|
||||
const [showImagePreview, setShowImagePreview] = useState(false);
|
||||
|
||||
// 预览图片 - 直接使用 require 资源
|
||||
const imageSource = require('@/assets/images/medicine/medicine-ai-summary.png');
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
translateY.setValue(screenHeight);
|
||||
backdropOpacity.setValue(0);
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(backdropOpacity, {
|
||||
toValue: 1,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.spring(translateY, {
|
||||
toValue: 0,
|
||||
useNativeDriver: true,
|
||||
bounciness: 6,
|
||||
speed: 12,
|
||||
}),
|
||||
]).start();
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.timing(backdropOpacity, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(translateY, {
|
||||
toValue: screenHeight,
|
||||
duration: 240,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
translateY.setValue(screenHeight);
|
||||
backdropOpacity.setValue(0);
|
||||
});
|
||||
}
|
||||
}, [visible, backdropOpacity, translateY]);
|
||||
|
||||
// 处理Android返回键关闭图片预览
|
||||
useEffect(() => {
|
||||
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
if (showImagePreview) {
|
||||
setShowImagePreview(false);
|
||||
return true; // 阻止默认返回行为
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return () => backHandler.remove();
|
||||
}, [showImagePreview]);
|
||||
|
||||
// 处理图片预览
|
||||
const handleImagePreview = useCallback(() => {
|
||||
triggerLightHaptic();
|
||||
setShowImagePreview(true);
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
// 安全地执行触觉反馈,避免因触觉反馈失败导致页面卡顿
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch((error) => {
|
||||
console.warn('[AI_SUMMARY] Haptic feedback failed:', error);
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (loading) return;
|
||||
// 安全地执行触觉反馈,避免因触觉反馈失败导致页面卡顿
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch((error) => {
|
||||
console.warn('[AI_SUMMARY] Haptic feedback failed:', error);
|
||||
});
|
||||
onConfirm();
|
||||
};
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="none"
|
||||
onRequestClose={handleClose}
|
||||
statusBarTranslucent
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.backdrop,
|
||||
{
|
||||
opacity: backdropOpacity,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity style={StyleSheet.absoluteFillObject} activeOpacity={1} onPress={handleClose} />
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{
|
||||
transform: [{ translateY }],
|
||||
paddingBottom: Math.max(insets.bottom, 20),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.handle} />
|
||||
|
||||
{/* 图标和标题 */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Ionicons name="sparkles" size={24} color="#8B5CF6" />
|
||||
</View>
|
||||
<Text style={styles.title}>{t('medications.aiSummaryInfo.title')}</Text>
|
||||
</View>
|
||||
|
||||
{/* 介绍图片区域 */}
|
||||
<View style={styles.imageContainer}>
|
||||
<Image
|
||||
source={require('@/assets/images/medicine/medicine-ai-summary.png')}
|
||||
style={styles.introImage}
|
||||
contentFit='contain'
|
||||
/>
|
||||
{/* 右上角查看大图提示按钮 */}
|
||||
<TouchableOpacity
|
||||
style={styles.viewImageButton}
|
||||
onPress={handleImagePreview}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={styles.glassViewButton}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(255, 255, 255, 0.3)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="expand-outline" size={16} color="#333" />
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.glassViewButton, styles.fallbackViewButton]}>
|
||||
<Ionicons name="expand-outline" size={16} color="#333" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 功能介绍内容 */}
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.featureItem}>
|
||||
<View style={styles.featureIcon}>
|
||||
<Ionicons name="analytics" size={20} color="#8B5CF6" />
|
||||
</View>
|
||||
<View style={styles.featureContent}>
|
||||
<Text style={styles.featureTitle}>{t('medications.aiSummaryInfo.features.intelligent.title')}</Text>
|
||||
<Text style={styles.featureDescription}>
|
||||
{t('medications.aiSummaryInfo.features.intelligent.description')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.featureItem}>
|
||||
<View style={styles.featureIcon}>
|
||||
<Ionicons name="time" size={20} color="#8B5CF6" />
|
||||
</View>
|
||||
<View style={styles.featureContent}>
|
||||
<Text style={styles.featureTitle}>{t('medications.aiSummaryInfo.features.tracking.title')}</Text>
|
||||
<Text style={styles.featureDescription}>
|
||||
{t('medications.aiSummaryInfo.features.tracking.description')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.featureItem}>
|
||||
<View style={styles.featureIcon}>
|
||||
<Ionicons name="shield-checkmark" size={20} color="#8B5CF6" />
|
||||
</View>
|
||||
<View style={styles.featureContent}>
|
||||
<Text style={styles.featureTitle}>{t('medications.aiSummaryInfo.features.professional.title')}</Text>
|
||||
<Text style={styles.featureDescription}>
|
||||
{t('medications.aiSummaryInfo.features.professional.description')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 确认按钮 - 支持 Liquid Glass */}
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={handleConfirm}
|
||||
disabled={loading}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={styles.confirmButton}
|
||||
glassEffectStyle="regular"
|
||||
tintColor="rgba(139, 92, 246, 0.8)"
|
||||
isInteractive={true}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="arrow-forward" size={20} color="#fff" />
|
||||
<Text style={styles.confirmText}>{t('medications.aiSummaryInfo.confirmButton')}</Text>
|
||||
</>
|
||||
)}
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.confirmButton, styles.fallbackButton]}>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="arrow-forward" size={20} color="#fff" />
|
||||
<Text style={styles.confirmText}>{t('medications.aiSummaryInfo.confirmButton')}</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* 图片预览 */}
|
||||
<ImageViewing
|
||||
images={[imageSource]}
|
||||
imageIndex={0}
|
||||
visible={showImagePreview}
|
||||
onRequestClose={() => setShowImagePreview(false)}
|
||||
swipeToCloseEnabled={true}
|
||||
doubleTapToZoomEnabled={true}
|
||||
FooterComponent={() => (
|
||||
<View style={styles.imageViewerFooter}>
|
||||
<TouchableOpacity
|
||||
style={styles.imageViewerFooterButton}
|
||||
onPress={() => setShowImagePreview(false)}
|
||||
>
|
||||
<Text style={styles.imageViewerFooterButtonText}>{t('medications.detail.imageViewer.close')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(15, 23, 42, 0.45)',
|
||||
},
|
||||
sheet: {
|
||||
backgroundColor: '#fff',
|
||||
borderTopLeftRadius: 28,
|
||||
borderTopRightRadius: 28,
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 16,
|
||||
shadowOffset: { width: 0, height: -4 },
|
||||
elevation: 16,
|
||||
gap: 20,
|
||||
},
|
||||
handle: {
|
||||
width: 50,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: '#E5E7EB',
|
||||
alignSelf: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#F3E8FF',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
},
|
||||
imageContainer: {
|
||||
width: '100%',
|
||||
height: 380,
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#F9FAFB',
|
||||
},
|
||||
introImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 16,
|
||||
},
|
||||
contentContainer: {
|
||||
gap: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
featureItem: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
featureIcon: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: '#F3E8FF',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
featureContent: {
|
||||
flex: 1,
|
||||
},
|
||||
featureTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
marginBottom: 4,
|
||||
},
|
||||
featureDescription: {
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
color: '#6B7280',
|
||||
},
|
||||
actions: {
|
||||
marginTop: 8,
|
||||
},
|
||||
confirmButton: {
|
||||
height: 56,
|
||||
borderRadius: 18,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
overflow: 'hidden', // 保证玻璃边界圆角效果
|
||||
},
|
||||
fallbackButton: {
|
||||
backgroundColor: '#8B5CF6',
|
||||
shadowColor: 'rgba(139, 92, 246, 0.45)',
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 20,
|
||||
elevation: 6,
|
||||
},
|
||||
confirmText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
// 图片预览相关样式
|
||||
viewImageButton: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
zIndex: 1,
|
||||
},
|
||||
glassViewButton: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
fallbackViewButton: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
},
|
||||
imageViewerFooter: {
|
||||
position: 'absolute',
|
||||
bottom: 60,
|
||||
left: 20,
|
||||
right: 20,
|
||||
alignItems: 'center',
|
||||
zIndex: 1,
|
||||
},
|
||||
imageViewerFooterButton: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 20,
|
||||
},
|
||||
imageViewerFooterButtonText: {
|
||||
color: '#FFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
@@ -469,4 +469,75 @@ export const medications = {
|
||||
button: 'Got it!',
|
||||
},
|
||||
},
|
||||
};
|
||||
aiSummary: {
|
||||
title: 'AI Medication Summary',
|
||||
headerBadge: 'AI Insight',
|
||||
subtitle: 'Adherence and safety overview',
|
||||
overviewTitle: 'Adherence snapshot',
|
||||
keyInsights: 'AI key insight',
|
||||
refresh: 'DeepSeek is analyzing, please wait...',
|
||||
stats: {
|
||||
activePlans: 'Active plans',
|
||||
plannedDoses: 'Planned doses',
|
||||
takenDoses: 'Taken doses',
|
||||
completion: 'Overall completion',
|
||||
avgCompletion: 'Avg adherence',
|
||||
activeDays: 'Planned days',
|
||||
},
|
||||
badges: {
|
||||
adherence: 'Adherence',
|
||||
safety: 'Monitoring',
|
||||
},
|
||||
doseSummary: 'Completed {{taken}} / {{planned}}',
|
||||
daysLabel: '{{days}} day plan • {{times}} times/day',
|
||||
completionLabel: '{{value}}% completed',
|
||||
emptyTitle: 'No active medication plans',
|
||||
emptyDescription: 'Activate or add a plan to generate the AI summary.',
|
||||
error403: 'Free AI quota is used up, please upgrade to continue.',
|
||||
genericError: 'Unable to load AI summary, please try again later.',
|
||||
keyInsightPlaceholder: 'No AI insight available yet.',
|
||||
listTitle: 'Plan breakdown',
|
||||
updatedAt: 'Updated {{time}}',
|
||||
pillChip: 'Professional advice',
|
||||
retry: 'Retry',
|
||||
infoModal: {
|
||||
badge: 'Info',
|
||||
title: 'Refresh & Adherence',
|
||||
point1: '• Daily Generation: Based on active medication plans and actual check-in data, generated daily.',
|
||||
point2: '• Refresh Effect: Retrieves the latest plan vs. actual completion and AI analysis, no extra quota used.',
|
||||
point3: '• Adherence: Degree of following the plan (completion rate). Higher means better compliance and lower risk.',
|
||||
point4: '• Statistics: Only counts plans with isActive=true and not deleted; completion only counts records with status "taken".',
|
||||
button: 'Got it',
|
||||
},
|
||||
completionInfoModal: {
|
||||
badge: 'Calculation',
|
||||
title: 'Completion Calculation Logic',
|
||||
point1: '• Overall completion = (Total actual doses taken ÷ Total planned doses) × 100%',
|
||||
point2: '• Actual doses taken: Number of medication records marked as "taken"',
|
||||
point3: '• Planned doses: Total doses calculated from the plan start date to current date based on daily frequency',
|
||||
point4: '• Detailed calculation: (Current date - Start date + 1) × Daily doses, e.g.: Day 5 with 2 daily doses = 10 total planned doses',
|
||||
point5: '• Individual plan completion = (Actual doses taken for that plan ÷ Planned doses for that plan) × 100%',
|
||||
button: 'Understood',
|
||||
},
|
||||
},
|
||||
aiSummaryInfo: {
|
||||
title: 'AI Medication Summary',
|
||||
placeholderImage: 'Intro Image',
|
||||
viewImage: 'View Full Image',
|
||||
features: {
|
||||
intelligent: {
|
||||
title: 'Intelligent Analysis',
|
||||
description: 'AI deeply analyzes your medication records to provide personalized health recommendations',
|
||||
},
|
||||
tracking: {
|
||||
title: 'Trend Tracking',
|
||||
description: 'Long-term tracking of medication effects to help optimize treatment plans',
|
||||
},
|
||||
professional: {
|
||||
title: 'Professional & Reliable',
|
||||
description: 'Based on medical knowledge base, providing safe and reliable health analysis',
|
||||
},
|
||||
},
|
||||
confirmButton: 'Subscribe Now',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -469,4 +469,75 @@ export const medications = {
|
||||
button: '知道了!',
|
||||
},
|
||||
},
|
||||
};
|
||||
aiSummary: {
|
||||
title: 'AI 用药总结',
|
||||
headerBadge: 'AI 专业总结',
|
||||
subtitle: '依从性与安全重点',
|
||||
overviewTitle: '用药总览',
|
||||
keyInsights: 'AI 重点解读',
|
||||
refresh: 'DeepSeek 正在进行分析,请稍等',
|
||||
stats: {
|
||||
activePlans: '进行中计划',
|
||||
plannedDoses: '计划总次数',
|
||||
takenDoses: '已完成次数',
|
||||
completion: '总体完成度',
|
||||
avgCompletion: '平均依从度',
|
||||
activeDays: '计划天数',
|
||||
},
|
||||
badges: {
|
||||
adherence: '依从性',
|
||||
safety: '监测建议',
|
||||
},
|
||||
doseSummary: '已完成 {{taken}} / {{planned}} 次',
|
||||
daysLabel: '{{days}} 天计划 · 每日 {{times}} 次',
|
||||
completionLabel: '完成度 {{value}}%',
|
||||
emptyTitle: '暂无开启的用药计划',
|
||||
emptyDescription: '激活或新增计划后,将自动生成 AI 总结。',
|
||||
error403: '免费使用次数已用完,请开通会员获取更多使用次数',
|
||||
genericError: '获取AI总结失败,请稍后重试',
|
||||
keyInsightPlaceholder: '暂未生成解读',
|
||||
listTitle: '计划分解',
|
||||
updatedAt: '更新于 {{time}}',
|
||||
pillChip: '专业建议',
|
||||
retry: '重试',
|
||||
infoModal: {
|
||||
badge: '说明',
|
||||
title: '刷新规律 & 依从度',
|
||||
point1: '• 每日生成:基于当天已开启的用药计划与实际打卡数据,每天出一版总结。',
|
||||
point2: '• 刷新作用:重新获取最新的计划 vs 实际完成度和 AI 解读,不会扣额外次数。',
|
||||
point3: '• 依从度:按计划执行的程度(完成率)。越高代表越遵医嘱、风险越低。',
|
||||
point4: '• 统计口径:仅统计 isActive=true 且未删除的计划;完成次数只计状态为 taken 的记录。',
|
||||
button: '知道了',
|
||||
},
|
||||
completionInfoModal: {
|
||||
badge: '计算说明',
|
||||
title: '完成度计算逻辑',
|
||||
point1: '• 总体完成度 = 所有计划的实际服药次数总和 ÷ 所有计划的理论服药次数总和 × 100%',
|
||||
point2: '• 实际服药次数:标记为"已服用"的用药记录数量',
|
||||
point3: '• 理论服药次数:从计划开始时间到当前时间,按照每日服药频率计算的总次数',
|
||||
point4: '• 理论次数详细计算:(当前日期 - 开始日期 + 1) × 每日服药次数,例如:今天是第5天,每日2次,则理论次数为10次',
|
||||
point5: '• 单个计划完成度 = 该计划的已服药次数 ÷ 该计划的理论服药次数 × 100%',
|
||||
button: '了解了',
|
||||
},
|
||||
},
|
||||
aiSummaryInfo: {
|
||||
title: 'AI 用药总结',
|
||||
placeholderImage: '介绍图片',
|
||||
viewImage: '查看大图',
|
||||
features: {
|
||||
intelligent: {
|
||||
title: '智能分析',
|
||||
description: 'AI 深度分析您的用药记录,提供个性化健康建议',
|
||||
},
|
||||
tracking: {
|
||||
title: '趋势追踪',
|
||||
description: '长期追踪用药效果,帮助优化治疗方案',
|
||||
},
|
||||
professional: {
|
||||
title: '专业可靠',
|
||||
description: '基于医学知识库,提供安全可靠的健康分析',
|
||||
},
|
||||
},
|
||||
confirmButton: '我要订阅',
|
||||
},
|
||||
};
|
||||
|
||||
208
ios/Podfile.lock
208
ios/Podfile.lock
@@ -2684,128 +2684,128 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
EXApplication: a9d1c46d473d36f61302a9a81db2379441f3f094
|
||||
EXConstants: e6e50cdfcb4524f40121d1fdcff24e97b7dcd2fd
|
||||
EXImageLoader: e501c001bc40b8326605e82e6e80363c80fe06b5
|
||||
EXNotifications: 7aab54f0e5f3023122bc95699eaff7c52bacb559
|
||||
Expo: e852e4b236f95ef9fee36ea9ab20bc6f59c76a10
|
||||
ExpoAppleAuthentication: 414e4316f8e25a2afbc3943cf725579c910f24b8
|
||||
ExpoAsset: ee515c16290e521de1870dcdee66d78173fbc533
|
||||
ExpoBackgroundTask: e048da30cd2d669c5ba20d5d704bee8dd6da320c
|
||||
ExpoBlur: b5b7a26572b3c33a11f0b2aa2f95c17c4c393b76
|
||||
ExpoCamera: d1894aad960492f4881c1f83967167963365d937
|
||||
ExpoClipboard: 99109306a2d9ed2fbd16f6b856e6267b2afa8472
|
||||
ExpoFileSystem: 73a9f3f2e0affc61eba5b9326153f64870438af1
|
||||
ExpoFont: b881d43057dceb7b31ff767b24f612609e80f60f
|
||||
ExpoGlassEffect: 3d147d753d3bfe1a5d6b7920560e206e3e98c19e
|
||||
ExpoHaptics: b48d913e7e5f23816c6f130e525c9a6501b160b5
|
||||
ExpoHead: 16bab3395d4328e88e4282a4f6ef4f44b1225c8d
|
||||
ExpoImage: 6eb842cd07817402640545c41884dd7f5fbfbca5
|
||||
ExpoImagePicker: bd0a5c81d7734548f6908a480609257e85d19ea8
|
||||
ExpoKeepAwake: 3f5e3ac53627849174f3603271df8e08f174ed4a
|
||||
ExpoLinearGradient: f9e7182e5253d53b2de4134b69d70bbfc2d50588
|
||||
ExpoLinking: 50a65cd7beb6051ffc82f84ffb33961dd5e55a5b
|
||||
ExpoLocalization: 6c6f0f89ad2822001ab0bc2eb6d4d980c77f080c
|
||||
ExpoMediaLibrary: 648cee3f5dcba13410ec9cc8ac9a426e89a61a31
|
||||
ExpoModulesCore: e37f2bfc6f5b553989e1a67e15b7c5c8bbcac0cc
|
||||
ExpoQuickActions: 62b9db8a20618be1cc19efa3b562ac963c803d58
|
||||
ExpoSplashScreen: f46795cd52cdad65d30e54043f04c86401c4f85d
|
||||
ExpoSQLite: f9d1202877e12bfa78a58309a3977ee4ea0b1314
|
||||
ExpoSymbols: ef7b8ac77ac2d496b1bc3f0f7daf5e19c3a9933a
|
||||
ExpoSystemUI: 9441d46a8efbf9224d1b2e6b18042452ffd0ed79
|
||||
ExpoUI: 821b058da921ea4aa6172b36d080991ea6fb2fae
|
||||
ExpoWebBrowser: 51218ce6ef35ea769e33409aac87fea3df4b919d
|
||||
EXTaskManager: 53f87ed11659341c3f3f02c0041498ef293f5684
|
||||
EXApplication: 296622817d459f46b6c5fe8691f4aac44d2b79e7
|
||||
EXConstants: fd688cef4e401dcf798a021cfb5d87c890c30ba3
|
||||
EXImageLoader: 189e3476581efe3ad4d1d3fb4735b7179eb26f05
|
||||
EXNotifications: 7cff475adb5d7a255a9ea46bbd2589cb3b454506
|
||||
Expo: 111394d38f32be09385d4c7f70cc96d2da438d0d
|
||||
ExpoAppleAuthentication: bc9de6e9ff3340604213ab9031d4c4f7f802623e
|
||||
ExpoAsset: d839c8eae8124470332408427327e8f88beb2dfd
|
||||
ExpoBackgroundTask: e0d201d38539c571efc5f9cb661fae8ab36ed61b
|
||||
ExpoBlur: 2dd8f64aa31f5d405652c21d3deb2d2588b1852f
|
||||
ExpoCamera: 2a87c210f8955350ea5c70f1d539520b2fc5d940
|
||||
ExpoClipboard: af650d14765f19c60ce2a1eaf9dfe6445eff7365
|
||||
ExpoFileSystem: 77157a101e03150a4ea4f854b4dd44883c93ae0a
|
||||
ExpoFont: cf9d90ec1d3b97c4f513211905724c8171f82961
|
||||
ExpoGlassEffect: 265fa3d75b46bc58262e4dfa513135fa9dfe4aac
|
||||
ExpoHaptics: 807476b0c39e9d82b7270349d6487928ce32df84
|
||||
ExpoHead: 95a6ee0be1142320bccf07961d6a1502ded5d6ac
|
||||
ExpoImage: 9c3428921c536ab29e5c6721d001ad5c1f469566
|
||||
ExpoImagePicker: d251aab45a1b1857e4156fed88511b278b4eee1c
|
||||
ExpoKeepAwake: 1a2e820692e933c94a565ec3fbbe38ac31658ffe
|
||||
ExpoLinearGradient: a464898cb95153125e3b81894fd479bcb1c7dd27
|
||||
ExpoLinking: 77455aa013e9b6a3601de03ecfab09858ee1b031
|
||||
ExpoLocalization: b852a5d8ec14c5349c1593eca87896b5b3ebfcca
|
||||
ExpoMediaLibrary: 641a6952299b395159ccd459bd8f5f6764bf55fe
|
||||
ExpoModulesCore: e8ec7f8727caf51a49d495598303dd420ca994bf
|
||||
ExpoQuickActions: 31a70aa6a606128de4416a4830e09cfabfe6667f
|
||||
ExpoSplashScreen: 268b2f128dc04284c21010540a6c4dd9f95003e3
|
||||
ExpoSQLite: 7fa091ba5562474093fef09be644161a65e11b3f
|
||||
ExpoSymbols: 1ae04ce686de719b9720453b988d8bc5bf776c68
|
||||
ExpoSystemUI: 2761aa6875849af83286364811d46e8ed8ea64c7
|
||||
ExpoUI: b99a1d1ef5352a60bebf4f4fd3a50d2f896ae804
|
||||
ExpoWebBrowser: d04a0d6247a0bea4519fbc2ea816610019ad83e0
|
||||
EXTaskManager: cbbb80cbccea6487ccca0631809fbba2ed3e5271
|
||||
FBLazyVector: e95a291ad2dadb88e42b06e0c5fb8262de53ec12
|
||||
hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172
|
||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
|
||||
lottie-react-native: 97a11537edc72d0763edab0c83e8cc8a0b9d8484
|
||||
lottie-react-native: cbe3d931a7c24f7891a8e8032c2bb9b2373c4b9c
|
||||
PurchasesHybridCommon: a4837eebc889b973668af685d6c23b89a038461d
|
||||
RCTDeprecation: 943572d4be82d480a48f4884f670135ae30bf990
|
||||
RCTRequired: 8f3cfc90cc25cf6e420ddb3e7caaaabc57df6043
|
||||
RCTTypeSafety: 16a4144ca3f959583ab019b57d5633df10b5e97c
|
||||
React: 914f8695f9bf38e6418228c2ffb70021e559f92f
|
||||
React-callinvoker: 1c0808402aee0c6d4a0d8e7220ce6547af9fba71
|
||||
React-Core: 4ae98f9e8135b8ddbd7c98730afb6fdae883db90
|
||||
React-Core-prebuilt: 8f4cca589c14e8cf8fc6db4587ef1c2056b5c151
|
||||
React-CoreModules: e878a90bb19b8f3851818af997dbae3b3b0a27ac
|
||||
React-cxxreact: 28af9844f6dc87be1385ab521fbfb3746f19563c
|
||||
React-Core: c61410ef0ca6055e204a963992e363227e0fd1c5
|
||||
React-Core-prebuilt: 02f0ad625ddd47463c009c2d0c5dd35c0d982599
|
||||
React-CoreModules: 1f6d1744b5f9f2ec684a4bb5ced25370f87e5382
|
||||
React-cxxreact: 3af79478e8187b63ffc22b794cd42d3fc1f1f2da
|
||||
React-debug: 6328c2228e268846161f10082e80dc69eac2e90a
|
||||
React-defaultsnativemodule: afc9d809ec75780f39464a6949c07987fbea488c
|
||||
React-domnativemodule: 91a233260411d41f27f67aa1358b7f9f0bfd101d
|
||||
React-Fabric: 21f349b5e93f305a3c38c885902683a9c79cf983
|
||||
React-FabricComponents: 47ac634cc9ecc64b30a9997192f510eebe4177e4
|
||||
React-FabricImage: 21873acd6d4a51a0b97c133141051c7acb11cc86
|
||||
React-featureflags: 653f469f0c3c9dc271d610373e3b6e66a9fd847d
|
||||
React-featureflagsnativemodule: c91a8a3880e0f4838286402241ead47db43aed28
|
||||
React-graphics: b4bdb0f635b8048c652a5d2b73eb8b1ddd950f24
|
||||
React-hermes: fcfad3b917400f49026f3232561e039c9d1c34bf
|
||||
React-idlecallbacksnativemodule: 8cb83207e39f8179ac1d344b6177c6ab3ccebcdc
|
||||
React-ImageManager: 396128004783fc510e629124dce682d38d1088e7
|
||||
React-jserrorhandler: b58b788d788cdbf8bda7db74a88ebfcffc8a0795
|
||||
React-jsi: d2c3f8555175371c02da6dfe7ed1b64b55a9d6c0
|
||||
React-jsiexecutor: ba537434eb45ee018b590ed7d29ee233fddb8669
|
||||
React-jsinspector: f21b6654baf96cb9f71748844a32468a5f73ad51
|
||||
React-jsinspectorcdp: 3f8be4830694c3c1c39442e50f8db877966d43f0
|
||||
React-jsinspectornetwork: 70e41469565712ad60e11d9c8b8f999b9f7f61eb
|
||||
React-jsinspectortracing: eccf9bfa4ec7f130d514f215cfb2222dc3c0e270
|
||||
React-jsitooling: b376a695f5a507627f7934748533b24eed1751ca
|
||||
React-jsitracing: 5c8c3273dda2d95191cc0612fb5e71c4d9018d2a
|
||||
React-logger: c3e2f8a2e284341205f61eef3d4677ab5a309dfd
|
||||
React-Mapbuffer: 603c18db65844bb81dbe62fee8fcc976eaeb7108
|
||||
React-microtasksnativemodule: d77e0c426fce34c23227394c96ca1033b30c813c
|
||||
react-native-render-html: 984dfe2294163d04bf5fe25d7c9f122e60e05ebe
|
||||
react-native-safe-area-context: add9b4ba236fe95ec600604d0fc72f395433dd59
|
||||
react-native-view-shot: 26174e54ec6b4b7c5d70b86964b747919759adc1
|
||||
react-native-voice: f5e8eec2278451d0017eb6a30a6ccc726aca34e0
|
||||
react-native-webview: a4f0775a31b73cf13cfc3d2d2b119aa94ec76e49
|
||||
React-NativeModulesApple: 1664340b8750d64e0ef3907c5e53d9481f74bcbd
|
||||
React-defaultsnativemodule: d635ef36d755321e5d6fc065bd166b2c5a0e9833
|
||||
React-domnativemodule: dd28f6d96cd21236e020be2eff6fe0b7d4ec3b66
|
||||
React-Fabric: 2e32c3fdbb1fbcf5fde54607e3abe453c6652ce2
|
||||
React-FabricComponents: 5ed0cdb81f6b91656cb4d3be432feaa28a58071a
|
||||
React-FabricImage: 2bc714f818cb24e454f5d3961864373271b2faf8
|
||||
React-featureflags: 847642f41fa71ad4eec5e0351badebcad4fe6171
|
||||
React-featureflagsnativemodule: c868a544b2c626fa337bcbd364b1befe749f0d3f
|
||||
React-graphics: 192ec701def5b3f2a07db2814dfba5a44986cff6
|
||||
React-hermes: e875778b496c86d07ab2ccaa36a9505d248a254b
|
||||
React-idlecallbacksnativemodule: 4d57965cdf82c14ee3b337189836cd8491632b76
|
||||
React-ImageManager: bd0b99e370b13de82c9cd15f0f08144ff3de079e
|
||||
React-jserrorhandler: a2fdef4cbcfdcdf3fa9f5d1f7190f7fd4535248d
|
||||
React-jsi: 89d43d1e7d4d0663f8ba67e0b39eb4e4672c27de
|
||||
React-jsiexecutor: abe4874aaab90dfee5dec480680220b2f8af07e3
|
||||
React-jsinspector: a0b3e051aef842b0b2be2353790ae2b2a5a65a8f
|
||||
React-jsinspectorcdp: 6346013b2247c6263fbf5199adf4a8751e53bd89
|
||||
React-jsinspectornetwork: 26281aa50d49fc1ec93abf981d934698fa95714f
|
||||
React-jsinspectortracing: 55eedf6d57540507570259a778663b90060bbd6e
|
||||
React-jsitooling: 0e001113fa56d8498aa8ac28437ac0d36348e51a
|
||||
React-jsitracing: b713793eb8a5bbc4d86a84e9d9e5023c0f58cbaf
|
||||
React-logger: 50fdb9a8236da90c0b1072da5c32ee03aeb5bf28
|
||||
React-Mapbuffer: 9050ee10c19f4f7fca8963d0211b2854d624973e
|
||||
React-microtasksnativemodule: f775db9e991c6f3b8ccbc02bfcde22770f96e23b
|
||||
react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd
|
||||
react-native-safe-area-context: 42a1b4f8774b577d03b53de7326e3d5757fe9513
|
||||
react-native-view-shot: fb3c0774edb448f42705491802a455beac1502a2
|
||||
react-native-voice: 908a0eba96c8c3d643e4f98b7232c6557d0a6f9c
|
||||
react-native-webview: b29007f4723bca10872028067b07abacfa1cb35a
|
||||
React-NativeModulesApple: 8969913947d5b576de4ed371a939455a8daf28aa
|
||||
React-oscompat: ce47230ed20185e91de62d8c6d139ae61763d09c
|
||||
React-perflogger: b1af3cfb3f095f819b2814910000392a8e17ba9f
|
||||
React-performancetimeline: f9ec65b77bcadbc7bd8b47a6f4b4b697da7b1490
|
||||
React-perflogger: 02b010e665772c7dcb859d85d44c1bfc5ac7c0e4
|
||||
React-performancetimeline: 130db956b5a83aa4fb41ddf5ae68da89f3fb1526
|
||||
React-RCTActionSheet: 0b14875b3963e9124a5a29a45bd1b22df8803916
|
||||
React-RCTAnimation: 60f6eca214a62b9673f64db6df3830cee902b5af
|
||||
React-RCTAppDelegate: 37734b39bac108af30a0fd9d3e1149ec68b82c28
|
||||
React-RCTBlob: 83fbcbd57755caf021787324aac2fe9b028cc264
|
||||
React-RCTFabric: a05cb1df484008db3753c8b4a71e4c6d9f1e43a6
|
||||
React-RCTFBReactNativeSpec: d58d7ae9447020bbbac651e3b0674422aba18266
|
||||
React-RCTImage: 47aba3be7c6c64f956b7918ab933769602406aac
|
||||
React-RCTLinking: 2dbaa4df2e4523f68baa07936bd8efdfa34d5f31
|
||||
React-RCTNetwork: 1fca7455f9dedf7de2b95bec438da06680f3b000
|
||||
React-RCTRuntime: 17819dd1dfc8613efaf4cbb9d8686baae4a83e5b
|
||||
React-RCTSettings: 01bf91c856862354d3d2f642ccb82f3697a4284a
|
||||
React-RCTText: cb576a3797dcb64933613c522296a07eaafc0461
|
||||
React-RCTVibration: 560af8c086741f3525b8456a482cdbe27f9d098e
|
||||
React-RCTAnimation: a7b90fd2af7bb9c084428867445a1481a8cb112e
|
||||
React-RCTAppDelegate: 3262bedd01263f140ec62b7989f4355f57cec016
|
||||
React-RCTBlob: c17531368702f1ebed5d0ada75a7cf5915072a53
|
||||
React-RCTFabric: 6409edd8cfdc3133b6cc75636d3b858fdb1d11ea
|
||||
React-RCTFBReactNativeSpec: c004b27b4fa3bd85878ad2cf53de3bbec85da797
|
||||
React-RCTImage: c68078a120d0123f4f07a5ac77bea3bb10242f32
|
||||
React-RCTLinking: cf8f9391fe7fe471f96da3a5f0435235eca18c5b
|
||||
React-RCTNetwork: ca31f7c879355760c2d9832a06ee35f517938a20
|
||||
React-RCTRuntime: a6cf4a1e42754fc87f493e538f2ac6b820e45418
|
||||
React-RCTSettings: e0e140b2ff4bf86d34e9637f6316848fc00be035
|
||||
React-RCTText: 75915bace6f7877c03a840cc7b6c622fb62bfa6b
|
||||
React-RCTVibration: 25f26b85e5e432bb3c256f8b384f9269e9529f25
|
||||
React-rendererconsistency: 2dac03f448ff337235fd5820b10f81633328870d
|
||||
React-renderercss: c5c6b7a15948dd28facca39a18ac269073718490
|
||||
React-rendererdebug: 3c9d5e1634273f5a24d84cc5669f290ce0bdc812
|
||||
React-RuntimeApple: 887637d1e12ea8262df7d32bc100467df2302613
|
||||
React-RuntimeCore: 91f779835dc4f8f84777fe5dd24f1a22f96454e4
|
||||
React-runtimeexecutor: 8bb6b738f37b0ada4a6269e6f8ab1133dea0285c
|
||||
React-RuntimeHermes: 4cb93de9fa8b1cc753d200dbe61a01b9ec5f5562
|
||||
React-runtimescheduler: 83dc28f530bfbd2fce84ed13aa7feebdc24e5af7
|
||||
React-timing: 03c7217455d2bff459b27a3811be25796b600f47
|
||||
React-utils: 6d46795ae0444ec8a5d9a5f201157b286bf5250a
|
||||
ReactAppDependencyProvider: c277c5b231881ad4f00cd59e3aa0671b99d7ebee
|
||||
ReactCodegen: 4c44b74b77fc41ae25b9e2c7e9bd6e2bc772c23f
|
||||
ReactCommon: e6e232202a447d353e5531f2be82f50f47cbaa9a
|
||||
React-renderercss: 477da167bb96b5ac86d30c5d295412fb853f5453
|
||||
React-rendererdebug: 2a1798c6f3ef5f22d466df24c33653edbabb5b89
|
||||
React-RuntimeApple: 28cf4d8eb18432f6a21abbed7d801ab7f6b6f0b4
|
||||
React-RuntimeCore: 41bf0fd56a00de5660f222415af49879fa49c4f0
|
||||
React-runtimeexecutor: 1afb774dde3011348e8334be69d2f57a359ea43e
|
||||
React-RuntimeHermes: f3b158ea40e8212b1a723a68b4315e7a495c5fc6
|
||||
React-runtimescheduler: 3e1e2bec7300bae512533107d8e54c6e5c63fe0f
|
||||
React-timing: 6fa9883de2e41791e5dc4ec404e5e37f3f50e801
|
||||
React-utils: 6e2035b53d087927768649a11a26c4e092448e34
|
||||
ReactAppDependencyProvider: 1bcd3527ac0390a1c898c114f81ff954be35ed79
|
||||
ReactCodegen: 7d4593f7591f002d137fe40cef3f6c11f13c88cc
|
||||
ReactCommon: 08810150b1206cc44aecf5f6ae19af32f29151a8
|
||||
ReactNativeDependencies: 71ce9c28beb282aa720ea7b46980fff9669f428a
|
||||
RevenueCat: 1e61140a343a77dc286f171b3ffab99ca09a4b57
|
||||
RNCAsyncStorage: e85a99325df9eb0191a6ee2b2a842644c7eb29f4
|
||||
RNCMaskedView: 3c9d7586e2b9bbab573591dcb823918bc4668005
|
||||
RNCPicker: e0149590451d5eae242cf686014a6f6d808f93c7
|
||||
RNDateTimePicker: 5e0a759109b63ebc661a4714712361d2d07142fe
|
||||
RNDeviceInfo: 8b6fa8379062949dd79a009cf3d6b02a9c03ca59
|
||||
RNGestureHandler: 6a488ce85c88e82d8610db1108daf04e9b2d5162
|
||||
RNPurchases: 121c761620615ae624ba7a74e2e5b94254995e6c
|
||||
RNReanimated: 00771d2ba7c810124852092eb29c0d1edf50cc58
|
||||
RNScreens: dd61bc3a3e6f6901ad833efa411917d44827cf51
|
||||
RNSentry: 54f8041cd06d7ccf484171edde72f1b07323fb2e
|
||||
RNSVG: 2825ee146e0f6a16221e852299943e4cceef4528
|
||||
RNWorklets: 83609071441ac7d623f1e0e63b9043f4f345e2a2
|
||||
RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4
|
||||
RNCMaskedView: d2578d41c59b936db122b2798ba37e4722d21035
|
||||
RNCPicker: c8a3584b74133464ee926224463fcc54dfdaebca
|
||||
RNDateTimePicker: 19ffa303c4524ec0a2dfdee2658198451c16b7f1
|
||||
RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c
|
||||
RNGestureHandler: 2914750df066d89bf9d8f48a10ad5f0051108ac3
|
||||
RNPurchases: 5f3cd4fea5ef2b3914c925b2201dd5cecd31922f
|
||||
RNReanimated: 1442a577e066e662f0ce1cd1864a65c8e547aee0
|
||||
RNScreens: d8d6f1792f6e7ac12b0190d33d8d390efc0c1845
|
||||
RNSentry: 1d7b9fdae7a01ad8f9053335b5d44e75c39a955e
|
||||
RNSVG: 31d6639663c249b7d5abc9728dde2041eb2a3c34
|
||||
RNWorklets: 54d8dffb7f645873a58484658ddfd4bd1a9a0bc1
|
||||
SDWebImage: d0184764be51240d49c761c37f53dd017e1ccaaf
|
||||
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
|
||||
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
||||
|
||||
@@ -39,6 +39,7 @@ export enum ChallengeType {
|
||||
MOOD = 'mood',
|
||||
SLEEP = 'sleep',
|
||||
WEIGHT = 'weight',
|
||||
STEP = 'step',
|
||||
CUSTOM = 'custom',
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
DailyMedicationStats,
|
||||
Medication,
|
||||
MedicationAiAnalysisV2,
|
||||
MedicationAiSummary,
|
||||
MedicationForm,
|
||||
MedicationRecognitionTask,
|
||||
MedicationRecord,
|
||||
@@ -315,6 +316,14 @@ export const getOverallStats = async (): Promise<{
|
||||
|
||||
// ==================== AI 分析相关 ====================
|
||||
|
||||
/**
|
||||
* 获取 AI 用药总结
|
||||
* @returns 当前激活用药计划的 AI 关键解读与完成度
|
||||
*/
|
||||
export const getMedicationAiSummary = async (): Promise<MedicationAiSummary> => {
|
||||
return api.get<MedicationAiSummary>('/medications/ai-summary');
|
||||
};
|
||||
|
||||
/**
|
||||
* 流式获取药品 AI 分析
|
||||
* @param medicationId 药品 ID
|
||||
|
||||
@@ -189,9 +189,13 @@ export class NotificationService {
|
||||
} else if (data?.type === 'goal_achievement') {
|
||||
// 处理目标达成通知
|
||||
console.log('用户点击了目标达成通知');
|
||||
} else if (data?.type === 'mood_checkin') {
|
||||
} else if (data?.type === 'mood_checkin' || data?.type === 'mood_checkin_reminder') {
|
||||
// 处理心情打卡提醒
|
||||
console.log('用户点击了心情打卡提醒');
|
||||
router.push({
|
||||
pathname: '/mood/edit',
|
||||
params: { date: new Date().toISOString().split('T')[0] }
|
||||
} as any);
|
||||
} else if (data?.type === 'goal_reminder') {
|
||||
// 处理目标提醒通知
|
||||
console.log('用户点击了目标提醒通知', data);
|
||||
@@ -221,13 +225,21 @@ export class NotificationService {
|
||||
if (data?.url) {
|
||||
router.push(data.url as any);
|
||||
}
|
||||
} else if (data?.type === 'water_reminder' || data?.type === 'regular_water_reminder') {
|
||||
} else if (data?.type === 'water_reminder' || data?.type === 'regular_water_reminder' || data?.type === 'custom_water_reminder') {
|
||||
// 处理喝水提醒通知
|
||||
console.log('用户点击了喝水提醒通知', data);
|
||||
// 跳转到统计页面查看喝水进度
|
||||
if (data?.url) {
|
||||
router.push(data.url as any);
|
||||
}
|
||||
} else if (data?.type === 'daily_summary' || data?.type === 'daily_summary_reminder') {
|
||||
// 处理每日总结通知
|
||||
console.log('用户点击了每日总结通知', data);
|
||||
if (data?.url) {
|
||||
router.push(data.url as any);
|
||||
} else {
|
||||
router.push('/(tabs)/statistics' as any);
|
||||
}
|
||||
} else if (data?.type === NotificationTypes.FASTING_START || data?.type === NotificationTypes.FASTING_END) {
|
||||
router.push(ROUTES.TAB_FASTING as any);
|
||||
} else if (data?.type === NotificationTypes.WORKOUT_COMPLETION) {
|
||||
|
||||
@@ -95,6 +95,28 @@ export interface MedicationDisplayItem {
|
||||
medicationId: string; // 药物ID
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 用药总结 - 药物层级统计
|
||||
*/
|
||||
export interface MedicationAiSummaryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
startDate: string;
|
||||
plannedDays: number;
|
||||
timesPerDay: number;
|
||||
plannedDoses: number;
|
||||
takenDoses: number;
|
||||
completionRate: number; // 0-1
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 用药总结响应
|
||||
*/
|
||||
export interface MedicationAiSummary {
|
||||
medicationAnalysis: MedicationAiSummaryItem[];
|
||||
keyInsights: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 药品 AI 分析 V2 结构化数据
|
||||
*/
|
||||
|
||||
@@ -359,7 +359,7 @@ export class MoodNotificationHelpers {
|
||||
data: {
|
||||
type: 'mood_reminder',
|
||||
isDailyReminder: true,
|
||||
url: '/mood-statistics' // 跳转到心情统计页面
|
||||
url: '/mood/edit' // 跳转到心情记录页面
|
||||
},
|
||||
sound: true,
|
||||
priority: 'normal',
|
||||
@@ -388,7 +388,7 @@ export class MoodNotificationHelpers {
|
||||
body: `${userName},夜深了~来记录一下今天的心情吧!每一份情感都值得被珍藏 ✨💕`,
|
||||
data: {
|
||||
type: 'mood_reminder',
|
||||
url: '/mood-statistics'
|
||||
url: '/mood/edit'
|
||||
},
|
||||
sound: true,
|
||||
priority: 'normal',
|
||||
|
||||
Reference in New Issue
Block a user