feat(medications): 优化药品管理功能和登录流程
- 更新默认药品图片为专用图标 - 移除未使用的 loading 状态选择器 - 优化 Apple 登录按钮样式,支持毛玻璃效果和加载状态 - 添加登录成功后返回功能(shouldBack 参数) - 药品详情页添加信息卡片点击交互 - 添加药品添加页面的登录状态检查 - 增强时间选择器错误处理和数据验证 - 修复药品图片显示逻辑,支持网络图片 - 优化药品卡片样式和布局 - 添加图片加载错误处理
This commit is contained in:
@@ -5,7 +5,7 @@ import { IconSymbol } from '@/components/ui/IconSymbol';
|
|||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
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 { DEFAULT_MEMBER_NAME } from '@/store/userSlice';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
@@ -42,7 +42,6 @@ export default function MedicationsScreen() {
|
|||||||
// 从 Redux 获取数据
|
// 从 Redux 获取数据
|
||||||
const selectedKey = selectedDate.format('YYYY-MM-DD');
|
const selectedKey = selectedDate.format('YYYY-MM-DD');
|
||||||
const medicationsForDay = useAppSelector((state) => selectMedicationDisplayItemsByDate(selectedKey)(state));
|
const medicationsForDay = useAppSelector((state) => selectMedicationDisplayItemsByDate(selectedKey)(state));
|
||||||
const loading = useAppSelector(selectMedicationsLoading);
|
|
||||||
|
|
||||||
const handleOpenAddMedication = useCallback(() => {
|
const handleOpenAddMedication = useCallback(() => {
|
||||||
router.push('/medications/add-medication');
|
router.push('/medications/add-medication');
|
||||||
@@ -79,9 +78,11 @@ export default function MedicationsScreen() {
|
|||||||
|
|
||||||
// 为每个药物添加默认图片(如果没有图片)
|
// 为每个药物添加默认图片(如果没有图片)
|
||||||
const medicationsWithImages = useMemo(() => {
|
const medicationsWithImages = useMemo(() => {
|
||||||
|
console.log('medicationsForDay', medicationsForDay);
|
||||||
|
|
||||||
return medicationsForDay.map((med: any) => ({
|
return medicationsForDay.map((med: any) => ({
|
||||||
...med,
|
...med,
|
||||||
image: med.image || require('@/assets/images/icons/icon-healthy-diet.png'), // 默认使用瓶子图标
|
image: med.image || require('@/assets/images/medicine/image-medicine.png'), // 默认使用瓶子图标
|
||||||
}));
|
}));
|
||||||
}, [medicationsForDay]);
|
}, [medicationsForDay]);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
|||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
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 { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
@@ -18,7 +18,7 @@ import Toast from 'react-native-toast-message';
|
|||||||
|
|
||||||
export default function LoginScreen() {
|
export default function LoginScreen() {
|
||||||
const router = useRouter();
|
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 scheme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const color = Colors[scheme];
|
const color = Colors[scheme];
|
||||||
const pageBackground = scheme === 'light' ? color.pageBackgroundEmphasis : color.background;
|
const pageBackground = scheme === 'light' ? color.pageBackgroundEmphasis : color.background;
|
||||||
@@ -122,16 +122,24 @@ export default function LoginScreen() {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
// 登录成功后处理重定向
|
// 登录成功后处理重定向
|
||||||
const to = searchParams?.redirectTo as string | undefined;
|
const shouldBack = searchParams?.shouldBack === 'true';
|
||||||
const paramsJson = searchParams?.redirectParams as string | undefined;
|
|
||||||
let parsedParams: Record<string, any> | undefined;
|
if (shouldBack) {
|
||||||
if (paramsJson) {
|
// 如果设置了 shouldBack,直接返回上一页
|
||||||
try { parsedParams = JSON.parse(paramsJson); } catch { }
|
|
||||||
}
|
|
||||||
if (to) {
|
|
||||||
router.replace({ pathname: to, params: parsedParams } as any);
|
|
||||||
} else {
|
|
||||||
router.back();
|
router.back();
|
||||||
|
} else {
|
||||||
|
// 否则按照原有逻辑进行重定向
|
||||||
|
const to = searchParams?.redirectTo as string | undefined;
|
||||||
|
const paramsJson = searchParams?.redirectParams as string | undefined;
|
||||||
|
let parsedParams: Record<string, any> | 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) {
|
} catch (err: any) {
|
||||||
console.log('err.code', err.code);
|
console.log('err.code', err.code);
|
||||||
@@ -248,20 +256,55 @@ export default function LoginScreen() {
|
|||||||
|
|
||||||
{/* Apple 登录 */}
|
{/* Apple 登录 */}
|
||||||
{appleAvailable && (
|
{appleAvailable && (
|
||||||
<Pressable
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={() => guardAgreement(onAppleLogin)}
|
onPress={() => guardAgreement(onAppleLogin)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
style={({ pressed }) => [
|
activeOpacity={0.7}
|
||||||
styles.appleButton,
|
|
||||||
{ backgroundColor: '#000000' },
|
|
||||||
loading && { opacity: 0.7 },
|
|
||||||
pressed && { transform: [{ scale: 0.98 }] },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Ionicons name="logo-apple" size={22} color="#FFFFFF" style={{ marginRight: 10 }} />
|
{isLiquidGlassAvailable() ? (
|
||||||
<Text style={styles.appleText}>使用 Apple 登录</Text>
|
<GlassView
|
||||||
</Pressable>
|
style={styles.appleButton}
|
||||||
|
glassEffectStyle="regular"
|
||||||
|
tintColor="rgba(0, 0, 0, 0.8)"
|
||||||
|
isInteractive={true}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator
|
||||||
|
size="small"
|
||||||
|
color="#FFFFFF"
|
||||||
|
style={{ marginRight: 10 }}
|
||||||
|
/>
|
||||||
|
<Text style={styles.appleText}>登录中...</Text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Ionicons name="logo-apple" size={22} color="#FFFFFF" style={{ marginRight: 10 }} />
|
||||||
|
<Text style={styles.appleText}>使用 Apple 登录</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</GlassView>
|
||||||
|
) : (
|
||||||
|
<View style={[styles.appleButton, styles.appleButtonFallback, loading && { opacity: 0.7 }]}>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator
|
||||||
|
size="small"
|
||||||
|
color="#FFFFFF"
|
||||||
|
style={{ marginRight: 10 }}
|
||||||
|
/>
|
||||||
|
<Text style={styles.appleText}>登录中...</Text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Ionicons name="logo-apple" size={22} color="#FFFFFF" style={{ marginRight: 10 }} />
|
||||||
|
<Text style={styles.appleText}>使用 Apple 登录</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 协议勾选 */}
|
{/* 协议勾选 */}
|
||||||
@@ -343,12 +386,16 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
|
overflow: 'hidden',
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 8 },
|
shadowOffset: { width: 0, height: 8 },
|
||||||
shadowOpacity: 0.15,
|
shadowOpacity: 0.15,
|
||||||
shadowRadius: 12,
|
shadowRadius: 12,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
},
|
||||||
|
appleButtonFallback: {
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
},
|
||||||
appleText: {
|
appleText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const FORM_LABELS: Record<Medication['form'], string> = {
|
|||||||
other: '其他',
|
other: '其他',
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_IMAGE = require('@/assets/images/icons/icon-healthy-diet.png');
|
const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png');
|
||||||
|
|
||||||
type RecordsSummary = {
|
type RecordsSummary = {
|
||||||
takenCount: number;
|
takenCount: number;
|
||||||
@@ -413,6 +413,22 @@ export default function MedicationDetailScreen() {
|
|||||||
}
|
}
|
||||||
}, [deleteLoading, dispatch, medication, router]);
|
}, [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) {
|
if (!medicationId) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: colors.pageBackgroundEmphasis }]}>
|
<View style={[styles.container, { backgroundColor: colors.pageBackgroundEmphasis }]}>
|
||||||
@@ -489,12 +505,16 @@ export default function MedicationDetailScreen() {
|
|||||||
value={startDateLabel}
|
value={startDateLabel}
|
||||||
icon="calendar-outline"
|
icon="calendar-outline"
|
||||||
colors={colors}
|
colors={colors}
|
||||||
|
clickable={true}
|
||||||
|
onPress={handleStartDatePress}
|
||||||
/>
|
/>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
label="时间"
|
label="时间"
|
||||||
value={reminderTimes}
|
value={reminderTimes}
|
||||||
icon="time-outline"
|
icon="time-outline"
|
||||||
colors={colors}
|
colors={colors}
|
||||||
|
clickable={true}
|
||||||
|
onPress={handleTimePress}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.fullCard, { backgroundColor: colors.surface }]}>
|
<View style={[styles.fullCard, { backgroundColor: colors.surface }]}>
|
||||||
@@ -511,8 +531,22 @@ export default function MedicationDetailScreen() {
|
|||||||
|
|
||||||
<Section title="剂量与形式" color={colors.text}>
|
<Section title="剂量与形式" color={colors.text}>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<InfoCard label="每次剂量" value={dosageLabel} icon="medkit-outline" colors={colors} />
|
<InfoCard
|
||||||
<InfoCard label="剂型" value={formLabel} icon="cube-outline" colors={colors} />
|
label="每次剂量"
|
||||||
|
value={dosageLabel}
|
||||||
|
icon="medkit-outline"
|
||||||
|
colors={colors}
|
||||||
|
clickable={true}
|
||||||
|
onPress={handleDosagePress}
|
||||||
|
/>
|
||||||
|
<InfoCard
|
||||||
|
label="剂型"
|
||||||
|
value={formLabel}
|
||||||
|
icon="cube-outline"
|
||||||
|
colors={colors}
|
||||||
|
clickable={true}
|
||||||
|
onPress={handleFormPress}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
@@ -712,20 +746,35 @@ const InfoCard = ({
|
|||||||
value,
|
value,
|
||||||
icon,
|
icon,
|
||||||
colors,
|
colors,
|
||||||
|
onPress,
|
||||||
|
clickable = false,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
icon: keyof typeof Ionicons.glyphMap;
|
icon: keyof typeof Ionicons.glyphMap;
|
||||||
colors: (typeof Colors)[keyof typeof Colors];
|
colors: (typeof Colors)[keyof typeof Colors];
|
||||||
|
onPress?: () => void;
|
||||||
|
clickable?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const CardWrapper = clickable ? TouchableOpacity : View;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.infoCard, { backgroundColor: colors.surface }]}>
|
<CardWrapper
|
||||||
|
style={[styles.infoCard, { backgroundColor: colors.surface }]}
|
||||||
|
onPress={onPress}
|
||||||
|
activeOpacity={clickable ? 0.7 : 1}
|
||||||
|
>
|
||||||
|
{clickable && (
|
||||||
|
<View style={styles.infoCardArrow}>
|
||||||
|
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
<View style={styles.infoCardIcon}>
|
<View style={styles.infoCardIcon}>
|
||||||
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
||||||
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
||||||
</View>
|
</CardWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -823,6 +872,13 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 8,
|
shadowRadius: 8,
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
infoCardArrow: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 12,
|
||||||
|
right: 12,
|
||||||
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
infoCardIcon: {
|
infoCardIcon: {
|
||||||
width: 28,
|
width: 28,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { HeaderBar } from '@/components/ui/HeaderBar';
|
|||||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppDispatch } from '@/hooks/redux';
|
import { useAppDispatch } from '@/hooks/redux';
|
||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||||
import { createMedicationAction, fetchMedicationRecords, fetchMedications } from '@/store/medicationsSlice';
|
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 formatTime = (date: Date) => dayjs(date).format('HH:mm');
|
||||||
const getDefaultTimeByIndex = (index: number) => DEFAULT_TIME_PRESETS[index % DEFAULT_TIME_PRESETS.length];
|
const getDefaultTimeByIndex = (index: number) => DEFAULT_TIME_PRESETS[index % DEFAULT_TIME_PRESETS.length];
|
||||||
const createDateFromTime = (time: string) => {
|
const createDateFromTime = (time: string) => {
|
||||||
const [hour, minute] = time.split(':').map((val) => parseInt(val, 10));
|
try {
|
||||||
const next = new Date();
|
if (!time || typeof time !== 'string') {
|
||||||
next.setHours(hour || 0, minute || 0, 0, 0);
|
console.warn('[MEDICATION] Invalid time string provided:', time);
|
||||||
return next;
|
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() {
|
export default function AddMedicationScreen() {
|
||||||
@@ -103,6 +134,8 @@ export default function AddMedicationScreen() {
|
|||||||
const [medicationName, setMedicationName] = useState('');
|
const [medicationName, setMedicationName] = useState('');
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const { upload, uploading } = useCosUpload({ prefix: 'images/medications' });
|
const { upload, uploading } = useCosUpload({ prefix: 'images/medications' });
|
||||||
|
// 获取登录验证相关的功能
|
||||||
|
const { ensureLoggedIn } = useAuthGuard();
|
||||||
const softBorderColor = useMemo(() => withAlpha(colors.border, 0.45), [colors.border]);
|
const softBorderColor = useMemo(() => withAlpha(colors.border, 0.45), [colors.border]);
|
||||||
const fadedBorderFill = useMemo(() => withAlpha(colors.border, 0.2), [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]);
|
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);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 先检查用户是否已登录,如果未登录则跳转到登录页面
|
||||||
|
const isLoggedIn = await ensureLoggedIn({
|
||||||
|
shouldBack: true
|
||||||
|
});
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
// 未登录,ensureLoggedIn 已处理跳转,直接返回
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 构建药物数据,符合 CreateMedicationDto 接口
|
// 构建药物数据,符合 CreateMedicationDto 接口
|
||||||
const medicationData = {
|
const medicationData = {
|
||||||
name: medicationName.trim(),
|
name: medicationName.trim(),
|
||||||
@@ -289,6 +332,7 @@ export default function AddMedicationScreen() {
|
|||||||
startDate,
|
startDate,
|
||||||
note,
|
note,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
ensureLoggedIn,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handlePrev = useCallback(() => {
|
const handlePrev = useCallback(() => {
|
||||||
@@ -378,29 +422,51 @@ export default function AddMedicationScreen() {
|
|||||||
|
|
||||||
const openTimePicker = useCallback(
|
const openTimePicker = useCallback(
|
||||||
(index?: number) => {
|
(index?: number) => {
|
||||||
if (typeof index === 'number') {
|
try {
|
||||||
setEditingTimeIndex(index);
|
if (typeof index === 'number') {
|
||||||
setTimePickerDate(createDateFromTime(medicationTimes[index]));
|
if (index >= 0 && index < medicationTimes.length) {
|
||||||
} else {
|
setEditingTimeIndex(index);
|
||||||
setEditingTimeIndex(null);
|
setTimePickerDate(createDateFromTime(medicationTimes[index]));
|
||||||
setTimePickerDate(createDateFromTime(getDefaultTimeByIndex(medicationTimes.length)));
|
} 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]
|
[medicationTimes]
|
||||||
);
|
);
|
||||||
|
|
||||||
const confirmTime = useCallback(
|
const confirmTime = useCallback(
|
||||||
(date: Date) => {
|
(date: Date) => {
|
||||||
const nextValue = formatTime(date);
|
try {
|
||||||
setMedicationTimes((prev) => {
|
if (!date || isNaN(date.getTime())) {
|
||||||
if (editingTimeIndex == null) {
|
console.error('[MEDICATION] Invalid date provided to confirmTime');
|
||||||
return [...prev, nextValue];
|
setTimePickerVisible(false);
|
||||||
|
setEditingTimeIndex(null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return prev.map((time, idx) => (idx === editingTimeIndex ? nextValue : time));
|
|
||||||
});
|
const nextValue = formatTime(date);
|
||||||
setTimePickerVisible(false);
|
setMedicationTimes((prev) => {
|
||||||
setEditingTimeIndex(null);
|
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]
|
[editingTimeIndex]
|
||||||
);
|
);
|
||||||
@@ -915,15 +981,26 @@ export default function AddMedicationScreen() {
|
|||||||
visible={timePickerVisible}
|
visible={timePickerVisible}
|
||||||
transparent
|
transparent
|
||||||
animationType="fade"
|
animationType="fade"
|
||||||
onRequestClose={() => setTimePickerVisible(false)}
|
onRequestClose={() => {
|
||||||
|
setTimePickerVisible(false);
|
||||||
|
setEditingTimeIndex(null);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Pressable style={styles.pickerBackdrop} onPress={() => setTimePickerVisible(false)} />
|
<Pressable
|
||||||
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}
|
style={styles.pickerBackdrop}
|
||||||
>
|
onPress={() => {
|
||||||
|
setTimePickerVisible(false);
|
||||||
|
setEditingTimeIndex(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
|
||||||
|
<ThemedText style={[styles.modalTitle, { color: colors.text }]}>
|
||||||
|
{editingTimeIndex !== null ? '修改提醒时间' : '添加提醒时间'}
|
||||||
|
</ThemedText>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={timePickerDate}
|
value={timePickerDate}
|
||||||
mode="time"
|
mode="time"
|
||||||
display={Platform.OS === 'ios' ? 'spinner' : 'clock'}
|
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||||
onChange={(event, date) => {
|
onChange={(event, date) => {
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
if (date) setTimePickerDate(date);
|
if (date) setTimePickerDate(date);
|
||||||
|
|||||||
BIN
assets/images/medicine/image-medicine.png
Normal file
BIN
assets/images/medicine/image-medicine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -6,7 +6,7 @@ import { Ionicons } from '@expo/vector-icons';
|
|||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Alert, StyleSheet, TouchableOpacity, View } from 'react-native';
|
import { Alert, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
export type MedicationCardProps = {
|
export type MedicationCardProps = {
|
||||||
@@ -19,10 +19,16 @@ export type MedicationCardProps = {
|
|||||||
export function MedicationCard({ medication, colors, selectedDate, onOpenDetails }: MedicationCardProps) {
|
export function MedicationCard({ medication, colors, selectedDate, onOpenDetails }: MedicationCardProps) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
const scheduledDate = dayjs(`${selectedDate.format('YYYY-MM-DD')} ${medication.scheduledTime}`);
|
const scheduledDate = dayjs(`${selectedDate.format('YYYY-MM-DD')} ${medication.scheduledTime}`);
|
||||||
const timeDiffMinutes = scheduledDate.diff(dayjs(), 'minute');
|
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();
|
const statusChip = renderStatusBadge();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.card, { shadowColor: colors.text }]}
|
style={[styles.card, { shadowColor: colors.text }]}
|
||||||
@@ -179,7 +186,12 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
<View style={styles.cardContent}>
|
<View style={styles.cardContent}>
|
||||||
<View style={styles.thumbnailWrapper}>
|
<View style={styles.thumbnailWrapper}>
|
||||||
<View style={styles.thumbnailSurface}>
|
<View style={styles.thumbnailSurface}>
|
||||||
<Image source={medication.image} style={styles.thumbnailImage} />
|
<Image
|
||||||
|
source={medication.image}
|
||||||
|
style={styles.thumbnailImage}
|
||||||
|
onError={() => setImageError(true)}
|
||||||
|
key={medication.id} // 重新渲染时重置状态
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.infoSection}>
|
<View style={styles.infoSection}>
|
||||||
@@ -211,21 +223,17 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
borderRadius: 26,
|
borderRadius: 18,
|
||||||
shadowOpacity: 0.08,
|
|
||||||
shadowOffset: { width: 0, height: 12 },
|
|
||||||
shadowRadius: 24,
|
|
||||||
elevation: 2,
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
cardSurface: {
|
cardSurface: {
|
||||||
borderRadius: 26,
|
borderRadius: 18,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
cardBody: {
|
cardBody: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 10,
|
||||||
paddingBottom: 20,
|
paddingBottom: 10,
|
||||||
paddingTop: 28,
|
paddingTop: 10,
|
||||||
},
|
},
|
||||||
cardContent: {
|
cardContent: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -233,20 +241,20 @@ const styles = StyleSheet.create({
|
|||||||
gap: 20,
|
gap: 20,
|
||||||
},
|
},
|
||||||
thumbnailWrapper: {
|
thumbnailWrapper: {
|
||||||
width: 126,
|
width: 148,
|
||||||
height: 110,
|
height: 110,
|
||||||
},
|
},
|
||||||
thumbnailSurface: {
|
thumbnailSurface: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 22,
|
|
||||||
backgroundColor: '#F1F4FF',
|
backgroundColor: '#F1F4FF',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
borderRadius: 18,
|
||||||
},
|
},
|
||||||
thumbnailImage: {
|
thumbnailImage: {
|
||||||
width: '80%',
|
width: '70%',
|
||||||
height: '80%',
|
height: '70%',
|
||||||
resizeMode: 'contain',
|
resizeMode: 'contain',
|
||||||
},
|
},
|
||||||
infoSection: {
|
infoSection: {
|
||||||
@@ -281,7 +289,7 @@ const styles = StyleSheet.create({
|
|||||||
gap: 6,
|
gap: 6,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
height: 38,
|
height: 38,
|
||||||
borderRadius: 24,
|
borderRadius: 10,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
actionButtonUpcoming: {
|
actionButtonUpcoming: {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type RedirectParams = Record<string, string | number | boolean | undefined>;
|
|||||||
type EnsureOptions = {
|
type EnsureOptions = {
|
||||||
redirectTo?: string;
|
redirectTo?: string;
|
||||||
redirectParams?: RedirectParams;
|
redirectParams?: RedirectParams;
|
||||||
|
shouldBack?: boolean; // 登录成功后是否返回上一页而不是重定向
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useAuthGuard() {
|
export function useAuthGuard() {
|
||||||
@@ -28,12 +29,14 @@ export function useAuthGuard() {
|
|||||||
|
|
||||||
const redirectTo = options?.redirectTo ?? currentPath ?? ROUTES.TAB_STATISTICS;
|
const redirectTo = options?.redirectTo ?? currentPath ?? ROUTES.TAB_STATISTICS;
|
||||||
const paramsJson = options?.redirectParams ? JSON.stringify(options.redirectParams) : undefined;
|
const paramsJson = options?.redirectParams ? JSON.stringify(options.redirectParams) : undefined;
|
||||||
|
const shouldBack = options?.shouldBack;
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
pathname: '/auth/login',
|
pathname: '/auth/login',
|
||||||
params: {
|
params: {
|
||||||
redirectTo,
|
redirectTo,
|
||||||
...(paramsJson ? { redirectParams: paramsJson } : {}),
|
...(paramsJson ? { redirectParams: paramsJson } : {}),
|
||||||
|
...(shouldBack ? { shouldBack: 'true' } : {}),
|
||||||
},
|
},
|
||||||
} as any);
|
} as any);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -698,7 +698,7 @@ export const selectMedicationDisplayItemsByDate = (date: string) => (state: Root
|
|||||||
// 转换为展示项
|
// 转换为展示项
|
||||||
return records
|
return records
|
||||||
.map((record) => {
|
.map((record) => {
|
||||||
const medication = record.medication || medicationMap.get(record.medicationId);
|
const medication = medicationMap.get(record.medicationId);
|
||||||
if (!medication) return null;
|
if (!medication) return null;
|
||||||
|
|
||||||
// 格式化剂量
|
// 格式化剂量
|
||||||
@@ -721,6 +721,7 @@ export const selectMedicationDisplayItemsByDate = (date: string) => (state: Root
|
|||||||
status: record.status,
|
status: record.status,
|
||||||
recordId: record.id,
|
recordId: record.id,
|
||||||
medicationId: medication.id,
|
medicationId: medication.id,
|
||||||
|
image: medication.photoUrl ? { uri: medication.photoUrl } : undefined
|
||||||
} as import('@/types/medication').MedicationDisplayItem;
|
} as import('@/types/medication').MedicationDisplayItem;
|
||||||
})
|
})
|
||||||
.filter((item): item is import('@/types/medication').MedicationDisplayItem => item !== null);
|
.filter((item): item is import('@/types/medication').MedicationDisplayItem => item !== null);
|
||||||
|
|||||||
Reference in New Issue
Block a user