diff --git a/app/(tabs)/medications.tsx b/app/(tabs)/medications.tsx index 14a5d94..98c1cb2 100644 --- a/app/(tabs)/medications.tsx +++ b/app/(tabs)/medications.tsx @@ -5,7 +5,7 @@ import { IconSymbol } from '@/components/ui/IconSymbol'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; -import { fetchMedicationRecords, fetchMedications, selectMedicationDisplayItemsByDate, selectMedicationsLoading } from '@/store/medicationsSlice'; +import { fetchMedicationRecords, fetchMedications, selectMedicationDisplayItemsByDate } from '@/store/medicationsSlice'; import { DEFAULT_MEMBER_NAME } from '@/store/userSlice'; import { useFocusEffect } from '@react-navigation/native'; import dayjs, { Dayjs } from 'dayjs'; @@ -42,7 +42,6 @@ export default function MedicationsScreen() { // 从 Redux 获取数据 const selectedKey = selectedDate.format('YYYY-MM-DD'); const medicationsForDay = useAppSelector((state) => selectMedicationDisplayItemsByDate(selectedKey)(state)); - const loading = useAppSelector(selectMedicationsLoading); const handleOpenAddMedication = useCallback(() => { router.push('/medications/add-medication'); @@ -79,9 +78,11 @@ export default function MedicationsScreen() { // 为每个药物添加默认图片(如果没有图片) const medicationsWithImages = useMemo(() => { + console.log('medicationsForDay', medicationsForDay); + return medicationsForDay.map((med: any) => ({ ...med, - image: med.image || require('@/assets/images/icons/icon-healthy-diet.png'), // 默认使用瓶子图标 + image: med.image || require('@/assets/images/medicine/image-medicine.png'), // 默认使用瓶子图标 })); }, [medicationsForDay]); diff --git a/app/auth/login.tsx b/app/auth/login.tsx index baa655f..5ca84f6 100644 --- a/app/auth/login.tsx +++ b/app/auth/login.tsx @@ -4,7 +4,7 @@ import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Alert, Animated, Linking, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { ActivityIndicator, Alert, Animated, Linking, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { ThemedText } from '@/components/ThemedText'; @@ -18,7 +18,7 @@ import Toast from 'react-native-toast-message'; export default function LoginScreen() { const router = useRouter(); - const searchParams = useLocalSearchParams<{ redirectTo?: string; redirectParams?: string }>(); + const searchParams = useLocalSearchParams<{ redirectTo?: string; redirectParams?: string; shouldBack?: string }>(); const scheme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const color = Colors[scheme]; const pageBackground = scheme === 'light' ? color.pageBackgroundEmphasis : color.background; @@ -122,16 +122,24 @@ export default function LoginScreen() { type: 'success', }); // 登录成功后处理重定向 - const to = searchParams?.redirectTo as string | undefined; - const paramsJson = searchParams?.redirectParams as string | undefined; - let parsedParams: Record | undefined; - if (paramsJson) { - try { parsedParams = JSON.parse(paramsJson); } catch { } - } - if (to) { - router.replace({ pathname: to, params: parsedParams } as any); - } else { + const shouldBack = searchParams?.shouldBack === 'true'; + + if (shouldBack) { + // 如果设置了 shouldBack,直接返回上一页 router.back(); + } else { + // 否则按照原有逻辑进行重定向 + const to = searchParams?.redirectTo as string | undefined; + const paramsJson = searchParams?.redirectParams as string | undefined; + let parsedParams: Record | undefined; + if (paramsJson) { + try { parsedParams = JSON.parse(paramsJson); } catch { } + } + if (to) { + router.replace({ pathname: to, params: parsedParams } as any); + } else { + router.back(); + } } } catch (err: any) { console.log('err.code', err.code); @@ -248,20 +256,55 @@ export default function LoginScreen() { {/* Apple 登录 */} {appleAvailable && ( - guardAgreement(onAppleLogin)} disabled={loading} - style={({ pressed }) => [ - styles.appleButton, - { backgroundColor: '#000000' }, - loading && { opacity: 0.7 }, - pressed && { transform: [{ scale: 0.98 }] }, - ]} + activeOpacity={0.7} > - - 使用 Apple 登录 - + {isLiquidGlassAvailable() ? ( + + {loading ? ( + <> + + 登录中... + + ) : ( + <> + + 使用 Apple 登录 + + )} + + ) : ( + + {loading ? ( + <> + + 登录中... + + ) : ( + <> + + 使用 Apple 登录 + + )} + + )} + )} {/* 协议勾选 */} @@ -343,12 +386,16 @@ const styles = StyleSheet.create({ justifyContent: 'center', flexDirection: 'row', marginBottom: 16, + overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.15, shadowRadius: 12, elevation: 2, }, + appleButtonFallback: { + backgroundColor: '#000000', + }, appleText: { fontSize: 16, color: '#FFFFFF', diff --git a/app/medications/[medicationId].tsx b/app/medications/[medicationId].tsx index 78be332..e504210 100644 --- a/app/medications/[medicationId].tsx +++ b/app/medications/[medicationId].tsx @@ -44,7 +44,7 @@ const FORM_LABELS: Record = { other: '其他', }; -const DEFAULT_IMAGE = require('@/assets/images/icons/icon-healthy-diet.png'); +const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png'); type RecordsSummary = { takenCount: number; @@ -413,6 +413,22 @@ export default function MedicationDetailScreen() { } }, [deleteLoading, dispatch, medication, router]); + const handleStartDatePress = useCallback(() => { + Alert.alert('开始日期', `开始服药日期:${startDateLabel}`); + }, [startDateLabel]); + + const handleTimePress = useCallback(() => { + Alert.alert('服药时间', `设置的时间:${reminderTimes}`); + }, [reminderTimes]); + + const handleDosagePress = useCallback(() => { + Alert.alert('每次剂量', `单次服用剂量:${dosageLabel}`); + }, [dosageLabel]); + + const handleFormPress = useCallback(() => { + Alert.alert('剂型', `药品剂型:${formLabel}`); + }, [formLabel]); + if (!medicationId) { return ( @@ -489,12 +505,16 @@ export default function MedicationDetailScreen() { value={startDateLabel} icon="calendar-outline" colors={colors} + clickable={true} + onPress={handleStartDatePress} /> @@ -511,8 +531,22 @@ export default function MedicationDetailScreen() {
- - + +
@@ -712,20 +746,35 @@ const InfoCard = ({ value, icon, colors, + onPress, + clickable = false, }: { label: string; value: string; icon: keyof typeof Ionicons.glyphMap; colors: (typeof Colors)[keyof typeof Colors]; + onPress?: () => void; + clickable?: boolean; }) => { + const CardWrapper = clickable ? TouchableOpacity : View; + return ( - + + {clickable && ( + + + + )} {label} {value} - + ); }; @@ -823,6 +872,13 @@ const styles = StyleSheet.create({ shadowRadius: 8, shadowOffset: { width: 0, height: 4 }, elevation: 2, + position: 'relative', + }, + infoCardArrow: { + position: 'absolute', + top: 12, + right: 12, + zIndex: 1, }, infoCardIcon: { width: 28, diff --git a/app/medications/add-medication.tsx b/app/medications/add-medication.tsx index 4207cb9..146212e 100644 --- a/app/medications/add-medication.tsx +++ b/app/medications/add-medication.tsx @@ -3,6 +3,7 @@ import { HeaderBar } from '@/components/ui/HeaderBar'; import { IconSymbol } from '@/components/ui/IconSymbol'; import { Colors } from '@/constants/Colors'; import { useAppDispatch } from '@/hooks/redux'; +import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useCosUpload } from '@/hooks/useCosUpload'; import { createMedicationAction, fetchMedicationRecords, fetchMedications } from '@/store/medicationsSlice'; @@ -86,10 +87,40 @@ const DEFAULT_TIME_PRESETS = ['08:00', '12:00', '18:00', '22:00']; const formatTime = (date: Date) => dayjs(date).format('HH:mm'); const getDefaultTimeByIndex = (index: number) => DEFAULT_TIME_PRESETS[index % DEFAULT_TIME_PRESETS.length]; const createDateFromTime = (time: string) => { - const [hour, minute] = time.split(':').map((val) => parseInt(val, 10)); - const next = new Date(); - next.setHours(hour || 0, minute || 0, 0, 0); - return next; + try { + if (!time || typeof time !== 'string') { + console.warn('[MEDICATION] Invalid time string provided:', time); + return new Date(); + } + + const parts = time.split(':'); + if (parts.length !== 2) { + console.warn('[MEDICATION] Invalid time format:', time); + return new Date(); + } + + const hour = parseInt(parts[0], 10); + const minute = parseInt(parts[1], 10); + + if (isNaN(hour) || isNaN(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) { + console.warn('[MEDICATION] Invalid time values:', { hour, minute }); + return new Date(); + } + + const next = new Date(); + next.setHours(hour, minute, 0, 0); + + // 验证日期是否有效 + if (isNaN(next.getTime())) { + console.error('[MEDICATION] Failed to create valid date'); + return new Date(); + } + + return next; + } catch (error) { + console.error('[MEDICATION] Error in createDateFromTime:', error); + return new Date(); + } }; export default function AddMedicationScreen() { @@ -103,6 +134,8 @@ export default function AddMedicationScreen() { const [medicationName, setMedicationName] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const { upload, uploading } = useCosUpload({ prefix: 'images/medications' }); + // 获取登录验证相关的功能 + const { ensureLoggedIn } = useAuthGuard(); const softBorderColor = useMemo(() => withAlpha(colors.border, 0.45), [colors.border]); const fadedBorderFill = useMemo(() => withAlpha(colors.border, 0.2), [colors.border]); const glassPrimaryTint = useMemo(() => withAlpha(colors.primary, theme === 'dark' ? 0.55 : 0.45), [colors.primary, theme]); @@ -231,6 +264,16 @@ export default function AddMedicationScreen() { setIsSubmitting(true); try { + // 先检查用户是否已登录,如果未登录则跳转到登录页面 + const isLoggedIn = await ensureLoggedIn({ + shouldBack: true + }); + if (!isLoggedIn) { + // 未登录,ensureLoggedIn 已处理跳转,直接返回 + setIsSubmitting(false); + return; + } + // 构建药物数据,符合 CreateMedicationDto 接口 const medicationData = { name: medicationName.trim(), @@ -289,6 +332,7 @@ export default function AddMedicationScreen() { startDate, note, dispatch, + ensureLoggedIn, ]); const handlePrev = useCallback(() => { @@ -378,29 +422,51 @@ export default function AddMedicationScreen() { const openTimePicker = useCallback( (index?: number) => { - if (typeof index === 'number') { - setEditingTimeIndex(index); - setTimePickerDate(createDateFromTime(medicationTimes[index])); - } else { - setEditingTimeIndex(null); - setTimePickerDate(createDateFromTime(getDefaultTimeByIndex(medicationTimes.length))); + try { + if (typeof index === 'number') { + if (index >= 0 && index < medicationTimes.length) { + setEditingTimeIndex(index); + setTimePickerDate(createDateFromTime(medicationTimes[index])); + } else { + console.error('[MEDICATION] Invalid time index:', index); + return; + } + } else { + setEditingTimeIndex(null); + setTimePickerDate(createDateFromTime(getDefaultTimeByIndex(medicationTimes.length))); + } + setTimePickerVisible(true); + } catch (error) { + console.error('[MEDICATION] Error in openTimePicker:', error); } - setTimePickerVisible(true); }, [medicationTimes] ); const confirmTime = useCallback( (date: Date) => { - const nextValue = formatTime(date); - setMedicationTimes((prev) => { - if (editingTimeIndex == null) { - return [...prev, nextValue]; + try { + if (!date || isNaN(date.getTime())) { + console.error('[MEDICATION] Invalid date provided to confirmTime'); + setTimePickerVisible(false); + setEditingTimeIndex(null); + return; } - return prev.map((time, idx) => (idx === editingTimeIndex ? nextValue : time)); - }); - setTimePickerVisible(false); - setEditingTimeIndex(null); + + const nextValue = formatTime(date); + setMedicationTimes((prev) => { + if (editingTimeIndex == null) { + return [...prev, nextValue]; + } + return prev.map((time, idx) => (idx === editingTimeIndex ? nextValue : time)); + }); + setTimePickerVisible(false); + setEditingTimeIndex(null); + } catch (error) { + console.error('[MEDICATION] Error in confirmTime:', error); + setTimePickerVisible(false); + setEditingTimeIndex(null); + } }, [editingTimeIndex] ); @@ -915,15 +981,26 @@ export default function AddMedicationScreen() { visible={timePickerVisible} transparent animationType="fade" - onRequestClose={() => setTimePickerVisible(false)} + onRequestClose={() => { + setTimePickerVisible(false); + setEditingTimeIndex(null); + }} > - setTimePickerVisible(false)} /> - + { + setTimePickerVisible(false); + setEditingTimeIndex(null); + }} + /> + + + {editingTimeIndex !== null ? '修改提醒时间' : '添加提醒时间'} + { if (Platform.OS === 'ios') { if (date) setTimePickerDate(date); diff --git a/assets/images/medicine/image-medicine.png b/assets/images/medicine/image-medicine.png new file mode 100644 index 0000000..9d665b5 Binary files /dev/null and b/assets/images/medicine/image-medicine.png differ diff --git a/components/medication/MedicationCard.tsx b/components/medication/MedicationCard.tsx index f1d507a..add0be5 100644 --- a/components/medication/MedicationCard.tsx +++ b/components/medication/MedicationCard.tsx @@ -6,7 +6,7 @@ import { Ionicons } from '@expo/vector-icons'; import dayjs, { Dayjs } from 'dayjs'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Alert, StyleSheet, TouchableOpacity, View } from 'react-native'; export type MedicationCardProps = { @@ -19,10 +19,16 @@ export type MedicationCardProps = { export function MedicationCard({ medication, colors, selectedDate, onOpenDetails }: MedicationCardProps) { const dispatch = useAppDispatch(); const [isSubmitting, setIsSubmitting] = useState(false); + const [imageError, setImageError] = useState(false); const scheduledDate = dayjs(`${selectedDate.format('YYYY-MM-DD')} ${medication.scheduledTime}`); const timeDiffMinutes = scheduledDate.diff(dayjs(), 'minute'); + // 当药品变化时重置图片错误状态 + useEffect(() => { + setImageError(false); + }, [medication.id]); + /** * 处理服药操作 */ @@ -166,6 +172,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails const statusChip = renderStatusBadge(); + return ( - + setImageError(true)} + key={medication.id} // 重新渲染时重置状态 + /> @@ -211,21 +223,17 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails const styles = StyleSheet.create({ card: { - borderRadius: 26, - shadowOpacity: 0.08, - shadowOffset: { width: 0, height: 12 }, - shadowRadius: 24, - elevation: 2, + borderRadius: 18, position: 'relative', }, cardSurface: { - borderRadius: 26, + borderRadius: 18, overflow: 'hidden', }, cardBody: { - paddingHorizontal: 20, - paddingBottom: 20, - paddingTop: 28, + paddingHorizontal: 10, + paddingBottom: 10, + paddingTop: 10, }, cardContent: { flexDirection: 'row', @@ -233,20 +241,20 @@ const styles = StyleSheet.create({ gap: 20, }, thumbnailWrapper: { - width: 126, + width: 148, height: 110, }, thumbnailSurface: { flex: 1, - borderRadius: 22, backgroundColor: '#F1F4FF', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', + borderRadius: 18, }, thumbnailImage: { - width: '80%', - height: '80%', + width: '70%', + height: '70%', resizeMode: 'contain', }, infoSection: { @@ -281,7 +289,7 @@ const styles = StyleSheet.create({ gap: 6, justifyContent: 'center', height: 38, - borderRadius: 24, + borderRadius: 10, overflow: 'hidden', }, actionButtonUpcoming: { diff --git a/hooks/useAuthGuard.ts b/hooks/useAuthGuard.ts index bbff00c..620e9b3 100644 --- a/hooks/useAuthGuard.ts +++ b/hooks/useAuthGuard.ts @@ -13,6 +13,7 @@ type RedirectParams = Record; type EnsureOptions = { redirectTo?: string; redirectParams?: RedirectParams; + shouldBack?: boolean; // 登录成功后是否返回上一页而不是重定向 }; export function useAuthGuard() { @@ -28,12 +29,14 @@ export function useAuthGuard() { const redirectTo = options?.redirectTo ?? currentPath ?? ROUTES.TAB_STATISTICS; const paramsJson = options?.redirectParams ? JSON.stringify(options.redirectParams) : undefined; + const shouldBack = options?.shouldBack; router.push({ pathname: '/auth/login', params: { redirectTo, ...(paramsJson ? { redirectParams: paramsJson } : {}), + ...(shouldBack ? { shouldBack: 'true' } : {}), }, } as any); return false; diff --git a/store/medicationsSlice.ts b/store/medicationsSlice.ts index af9a326..8340797 100644 --- a/store/medicationsSlice.ts +++ b/store/medicationsSlice.ts @@ -698,7 +698,7 @@ export const selectMedicationDisplayItemsByDate = (date: string) => (state: Root // 转换为展示项 return records .map((record) => { - const medication = record.medication || medicationMap.get(record.medicationId); + const medication = medicationMap.get(record.medicationId); if (!medication) return null; // 格式化剂量 @@ -721,6 +721,7 @@ export const selectMedicationDisplayItemsByDate = (date: string) => (state: Root status: record.status, recordId: record.id, medicationId: medication.id, + image: medication.photoUrl ? { uri: medication.photoUrl } : undefined } as import('@/types/medication').MedicationDisplayItem; }) .filter((item): item is import('@/types/medication').MedicationDisplayItem => item !== null);