feat(medications): 优化药品管理功能和登录流程

- 更新默认药品图片为专用图标
- 移除未使用的 loading 状态选择器
- 优化 Apple 登录按钮样式,支持毛玻璃效果和加载状态
- 添加登录成功后返回功能(shouldBack 参数)
- 药品详情页添加信息卡片点击交互
- 添加药品添加页面的登录状态检查
- 增强时间选择器错误处理和数据验证
- 修复药品图片显示逻辑,支持网络图片
- 优化药品卡片样式和布局
- 添加图片加载错误处理
This commit is contained in:
richarjiang
2025-11-11 10:02:37 +08:00
parent 0594831c9f
commit 50525f82a1
8 changed files with 263 additions and 70 deletions

View File

@@ -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]);

View File

@@ -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,6 +122,13 @@ export default function LoginScreen() {
type: 'success', type: 'success',
}); });
// 登录成功后处理重定向 // 登录成功后处理重定向
const shouldBack = searchParams?.shouldBack === 'true';
if (shouldBack) {
// 如果设置了 shouldBack直接返回上一页
router.back();
} else {
// 否则按照原有逻辑进行重定向
const to = searchParams?.redirectTo as string | undefined; const to = searchParams?.redirectTo as string | undefined;
const paramsJson = searchParams?.redirectParams as string | undefined; const paramsJson = searchParams?.redirectParams as string | undefined;
let parsedParams: Record<string, any> | undefined; let parsedParams: Record<string, any> | undefined;
@@ -133,6 +140,7 @@ export default function LoginScreen() {
} else { } else {
router.back(); 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 }] },
]}
> >
{isLiquidGlassAvailable() ? (
<GlassView
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 }} /> <Ionicons name="logo-apple" size={22} color="#FFFFFF" style={{ marginRight: 10 }} />
<Text style={styles.appleText}>使 Apple </Text> <Text style={styles.appleText}>使 Apple </Text>
</Pressable> </>
)}
</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',

View File

@@ -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,

View File

@@ -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 {
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(); const next = new Date();
next.setHours(hour || 0, minute || 0, 0, 0); next.setHours(hour, minute, 0, 0);
// 验证日期是否有效
if (isNaN(next.getTime())) {
console.error('[MEDICATION] Failed to create valid date');
return new Date();
}
return next; 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,20 +422,37 @@ export default function AddMedicationScreen() {
const openTimePicker = useCallback( const openTimePicker = useCallback(
(index?: number) => { (index?: number) => {
try {
if (typeof index === 'number') { if (typeof index === 'number') {
if (index >= 0 && index < medicationTimes.length) {
setEditingTimeIndex(index); setEditingTimeIndex(index);
setTimePickerDate(createDateFromTime(medicationTimes[index])); setTimePickerDate(createDateFromTime(medicationTimes[index]));
} else {
console.error('[MEDICATION] Invalid time index:', index);
return;
}
} else { } else {
setEditingTimeIndex(null); setEditingTimeIndex(null);
setTimePickerDate(createDateFromTime(getDefaultTimeByIndex(medicationTimes.length))); setTimePickerDate(createDateFromTime(getDefaultTimeByIndex(medicationTimes.length)));
} }
setTimePickerVisible(true); setTimePickerVisible(true);
} catch (error) {
console.error('[MEDICATION] Error in openTimePicker:', error);
}
}, },
[medicationTimes] [medicationTimes]
); );
const confirmTime = useCallback( const confirmTime = useCallback(
(date: Date) => { (date: Date) => {
try {
if (!date || isNaN(date.getTime())) {
console.error('[MEDICATION] Invalid date provided to confirmTime');
setTimePickerVisible(false);
setEditingTimeIndex(null);
return;
}
const nextValue = formatTime(date); const nextValue = formatTime(date);
setMedicationTimes((prev) => { setMedicationTimes((prev) => {
if (editingTimeIndex == null) { if (editingTimeIndex == null) {
@@ -401,6 +462,11 @@ export default function AddMedicationScreen() {
}); });
setTimePickerVisible(false); setTimePickerVisible(false);
setEditingTimeIndex(null); 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);
<Pressable style={styles.pickerBackdrop} onPress={() => setTimePickerVisible(false)} /> setEditingTimeIndex(null);
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]} }}
> >
<Pressable
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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -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: {

View File

@@ -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;

View File

@@ -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);