Files
digital-pilates/app/medications/[medicationId].tsx
richarjiang d9975813cb feat(medications): 添加药品图片预览功能并优化InfoCard组件
- 在药品详情页面集成 react-native-image-viewing 实现图片全屏预览
- 添加图片预览提示图标,提升用户交互体验
- 优化 InfoCard 组件渲染逻辑,简化代码结构
- 调整药品图片样式,增加圆角效果并优化尺寸比例
- 为可点击的 InfoCard 图标和箭头添加玻璃态效果支持
2025-11-11 14:40:26 +08:00

1807 lines
56 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ThemedText } from '@/components/ThemedText';
import { ConfirmationSheet } from '@/components/ui/ConfirmationSheet';
import { HeaderBar } from '@/components/ui/HeaderBar';
import InfoCard from '@/components/ui/InfoCard';
import { Colors } from '@/constants/Colors';
import { DOSAGE_UNITS, DOSAGE_VALUES, FORM_LABELS, FORM_OPTIONS, TIMES_PER_DAY_OPTIONS } from '@/constants/Medication';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { getMedicationById, getMedicationRecords } from '@/services/medications';
import {
deleteMedicationAction,
fetchMedications,
selectMedications,
updateMedicationAction,
} from '@/store/medicationsSlice';
import type { Medication, MedicationForm, RepeatPattern } from '@/types/medication';
import { Ionicons } from '@expo/vector-icons';
import DateTimePicker from '@react-native-community/datetimepicker';
import { Picker } from '@react-native-picker/picker';
import Voice from '@react-native-voice/voice';
import dayjs from 'dayjs';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Keyboard,
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
Switch,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import ImageViewing from 'react-native-image-viewing';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png');
type RecordsSummary = {
takenCount: number;
startedDays: number | null;
};
export default function MedicationDetailScreen() {
const params = useLocalSearchParams<{ medicationId?: string }>();
const medicationId = Array.isArray(params.medicationId)
? params.medicationId[0]
: params.medicationId;
const dispatch = useAppDispatch();
const scheme = (useColorScheme() ?? 'light') as keyof typeof Colors;
const colors = Colors[scheme];
const insets = useSafeAreaInsets();
const router = useRouter();
const medications = useAppSelector(selectMedications);
const medicationFromStore = medications.find((item) => item.id === medicationId);
const [medication, setMedication] = useState<Medication | null>(medicationFromStore ?? null);
const [loading, setLoading] = useState(!medicationFromStore);
const [summary, setSummary] = useState<RecordsSummary>({
takenCount: 0,
startedDays: null,
});
const [summaryLoading, setSummaryLoading] = useState(true);
const [updatePending, setUpdatePending] = useState(false);
const [error, setError] = useState<string | null>(null);
const [noteModalVisible, setNoteModalVisible] = useState(false);
const [noteDraft, setNoteDraft] = useState(medication?.note ?? '');
const [noteSaving, setNoteSaving] = useState(false);
const [dictationActive, setDictationActive] = useState(false);
const [dictationLoading, setDictationLoading] = useState(false);
const isDictationSupported = Platform.OS === 'ios';
const [keyboardHeight, setKeyboardHeight] = useState(0);
const [deleteSheetVisible, setDeleteSheetVisible] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const [showImagePreview, setShowImagePreview] = useState(false);
// 剂量选择相关状态
const [dosagePickerVisible, setDosagePickerVisible] = useState(false);
const [dosageValuePicker, setDosageValuePicker] = useState(
medicationFromStore?.dosageValue ?? 1
);
const [dosageUnitPicker, setDosageUnitPicker] = useState(
medicationFromStore?.dosageUnit ?? DOSAGE_UNITS[0]
);
// 剂型选择相关状态
const [formPickerVisible, setFormPickerVisible] = useState(false);
const [formPicker, setFormPicker] = useState<MedicationForm>(
medicationFromStore?.form ?? 'capsule'
);
// 频率选择相关状态
const [frequencyPickerVisible, setFrequencyPickerVisible] = useState(false);
const [repeatPatternPicker, setRepeatPatternPicker] = useState<RepeatPattern>(
medicationFromStore?.repeatPattern ?? 'daily'
);
const [timesPerDayPicker, setTimesPerDayPicker] = useState(
medicationFromStore?.timesPerDay ?? 1
);
// 提醒时间相关状态
const [timePickerVisible, setTimePickerVisible] = useState(false);
const [timePickerDate, setTimePickerDate] = useState<Date>(new Date());
const [editingTimeIndex, setEditingTimeIndex] = useState<number | null>(null);
const [medicationTimesPicker, setMedicationTimesPicker] = useState<string[]>(
medicationFromStore?.medicationTimes ?? []
);
// 辅助函数:从时间字符串创建 Date 对象
const createDateFromTime = useCallback((time: string) => {
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();
}
}, []);
// 辅助函数:格式化时间
const formatTime = useCallback((date: Date) => dayjs(date).format('HH:mm'), []);
// 辅助函数:获取默认时间
const getDefaultTimeByIndex = useCallback((index: number) => {
const DEFAULT_TIME_PRESETS = ['08:00', '12:00', '18:00', '22:00'];
return DEFAULT_TIME_PRESETS[index % DEFAULT_TIME_PRESETS.length];
}, []);
useEffect(() => {
if (!medicationFromStore) {
dispatch(fetchMedications());
}
}, [dispatch, medicationFromStore]);
useEffect(() => {
if (medicationFromStore) {
setMedication(medicationFromStore);
setLoading(false);
}
}, [medicationFromStore]);
useEffect(() => {
// 同步剂量选择器、剂型选择器、频率选择器和时间选择器的默认值
if (medication) {
setDosageValuePicker(medication.dosageValue);
setDosageUnitPicker(medication.dosageUnit);
setFormPicker(medication.form);
setRepeatPatternPicker(medication.repeatPattern);
setTimesPerDayPicker(medication.timesPerDay);
setMedicationTimesPicker(medication.medicationTimes || []);
}
}, [medication?.dosageValue, medication?.dosageUnit, medication?.form, medication?.repeatPattern, medication?.timesPerDay, medication?.medicationTimes]);
// 根据 timesPerDayPicker 动态调整 medicationTimesPicker与 add-medication.tsx 逻辑一致)
useEffect(() => {
setMedicationTimesPicker((prev) => {
if (timesPerDayPicker > prev.length) {
// 需要添加更多时间
const next = [...prev];
while (next.length < timesPerDayPicker) {
next.push(getDefaultTimeByIndex(next.length));
}
return next;
}
if (timesPerDayPicker < prev.length) {
// 需要删除多余时间
return prev.slice(0, timesPerDayPicker);
}
return prev;
});
}, [timesPerDayPicker, getDefaultTimeByIndex]);
useEffect(() => {
setNoteDraft(medication?.note ?? '');
}, [medication?.note]);
useEffect(() => {
let isMounted = true;
let abortController = new AbortController();
console.log('[MEDICATION_DETAIL] useEffect triggered', {
medicationId,
hasMedicationFromStore: !!medicationFromStore,
deleteLoading
});
// 如果正在删除操作中,不执行任何操作
if (deleteLoading) {
console.log('[MEDICATION_DETAIL] Delete operation in progress, skipping useEffect');
return () => {
isMounted = false;
abortController.abort();
};
}
if (!medicationId || medicationFromStore) {
console.log('[MEDICATION_DETAIL] Early return from useEffect', {
hasMedicationId: !!medicationId,
hasMedicationFromStore: !!medicationFromStore
});
return () => {
isMounted = false;
abortController.abort();
};
}
console.log('[MEDICATION_DETAIL] Starting API call for medication', medicationId);
setLoading(true);
getMedicationById(medicationId)
.then((data) => {
if (!isMounted || abortController.signal.aborted) return;
console.log('[MEDICATION_DETAIL] API call successful', data);
setMedication(data);
setError(null);
})
.catch((err) => {
if (abortController.signal.aborted) return;
console.error('加载药品详情失败', err);
console.log('[MEDICATION_DETAIL] API call failed for medication', medicationId, err);
if (isMounted) {
setError('暂时无法获取该药品的信息,请稍后重试。');
}
})
.finally(() => {
if (isMounted && !abortController.signal.aborted) {
setLoading(false);
}
});
return () => {
isMounted = false;
abortController.abort();
};
}, [medicationId, medicationFromStore, deleteLoading]);
useEffect(() => {
let isMounted = true;
if (!medicationId) {
return () => {
isMounted = false;
};
}
setSummaryLoading(true);
getMedicationRecords({ medicationId })
.then((records) => {
if (!isMounted) return;
const takenCount = records.filter((record) => record.status === 'taken').length;
const earliestRecord = records.reduce<Date | null>((earliest, record) => {
const current = new Date(record.scheduledTime);
if (!earliest || current < earliest) {
return current;
}
return earliest;
}, null);
const startedDaysRaw = earliestRecord
? dayjs().diff(dayjs(earliestRecord), 'day')
: medication
? dayjs().diff(dayjs(medication.startDate), 'day')
: null;
const startedDays = typeof startedDaysRaw === 'number'
? Math.max(startedDaysRaw, 0)
: null;
setSummary({
takenCount,
startedDays: startedDays ?? null,
});
})
.catch((err) => {
console.error('加载服药记录失败', err);
})
.finally(() => {
if (isMounted) {
setSummaryLoading(false);
}
});
return () => {
isMounted = false;
};
}, [medicationId, medication]);
const appendDictationResult = useCallback((text: string) => {
const clean = text.trim();
if (!clean) return;
setNoteDraft((prev) => {
if (!prev) return clean;
return `${prev}${prev.endsWith('\n') ? '' : '\n'}${clean}`;
});
}, []);
useEffect(() => {
if (!isDictationSupported || !noteModalVisible) {
return;
}
Voice.onSpeechStart = () => {
setDictationActive(true);
setDictationLoading(false);
};
Voice.onSpeechEnd = () => {
setDictationActive(false);
setDictationLoading(false);
};
Voice.onSpeechResults = (event: any) => {
const recognized = event?.value?.[0];
if (recognized) {
appendDictationResult(recognized);
}
};
Voice.onSpeechError = (error: any) => {
console.log('[MEDICATION_DETAIL] voice error', error);
setDictationActive(false);
setDictationLoading(false);
Alert.alert('语音识别不可用', '无法使用语音输入,请检查权限设置后重试');
};
return () => {
Voice.destroy()
.then(() => {
Voice.removeAllListeners();
})
.catch(() => {});
};
}, [appendDictationResult, isDictationSupported, noteModalVisible]);
useEffect(() => {
if (!noteModalVisible) {
setKeyboardHeight(0);
return;
}
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
const handleShow = (event: any) => {
const height = event?.endCoordinates?.height ?? 0;
setKeyboardHeight(height);
};
const handleHide = () => setKeyboardHeight(0);
const showSub = Keyboard.addListener(showEvent, handleShow);
const hideSub = Keyboard.addListener(hideEvent, handleHide);
return () => {
showSub.remove();
hideSub.remove();
};
}, [noteModalVisible]);
const handleDictationPress = useCallback(async () => {
if (!isDictationSupported || dictationLoading) {
return;
}
try {
if (dictationActive) {
setDictationLoading(true);
await Voice.stop();
setDictationLoading(false);
return;
}
setDictationLoading(true);
try {
await Voice.stop();
} catch {
// ignore if not recording
}
await Voice.start('zh-CN');
} catch (error) {
console.log('[MEDICATION_DETAIL] unable to start dictation', error);
setDictationLoading(false);
Alert.alert('无法启动语音输入', '请检查麦克风与语音识别权限后重试');
}
}, [dictationActive, dictationLoading, isDictationSupported]);
const closeNoteModal = useCallback(() => {
setNoteModalVisible(false);
if (dictationActive) {
Voice.stop().catch(() => {});
}
setDictationActive(false);
setDictationLoading(false);
setKeyboardHeight(0);
}, [dictationActive]);
const handleToggleMedication = async (nextValue: boolean) => {
if (!medication || updatePending) return;
try {
setUpdatePending(true);
const updated = await dispatch(
updateMedicationAction({
id: medication.id,
isActive: nextValue,
})
).unwrap();
setMedication(updated);
} catch (err) {
console.error('切换药品状态失败', err);
Alert.alert('操作失败', '切换提醒状态时出现问题,请稍后重试。');
} finally {
setUpdatePending(false);
}
};
const formLabel = medication ? FORM_LABELS[medication.form] : '';
const dosageLabel = medication ? `${medication.dosageValue} ${medication.dosageUnit}` : '--';
const startDateLabel = medication
? dayjs(medication.startDate).format('YYYY年M月D日')
: '--';
const reminderTimes = medication?.medicationTimes?.length
? medication.medicationTimes.join('、')
: '尚未设置';
const frequencyLabel = useMemo(() => {
if (!medication) return '--';
switch (medication.repeatPattern) {
case 'daily':
return `每日 ${medication.timesPerDay}`;
case 'weekly':
return `每周 ${medication.timesPerDay}`;
default:
return `自定义 · ${medication.timesPerDay} 次/日`;
}
}, [medication]);
const handleOpenNoteModal = useCallback(() => {
setNoteDraft(medication?.note ?? '');
setNoteModalVisible(true);
}, [medication?.note]);
const handleSaveNote = useCallback(async () => {
if (!medication) return;
const trimmed = noteDraft.trim();
setNoteSaving(true);
try {
const updated = await dispatch(
updateMedicationAction({
id: medication.id,
note: trimmed || undefined,
})
).unwrap();
setMedication(updated);
closeNoteModal();
} catch (err) {
console.error('保存备注失败', err);
Alert.alert('保存失败', '提交备注时出现问题,请稍后重试。');
} finally {
setNoteSaving(false);
}
}, [closeNoteModal, dispatch, medication, noteDraft]);
const statusLabel = medication?.isActive ? '提醒已开启' : '提醒已关闭';
const noteText = medication?.note?.trim() ? medication.note : '暂无备注信息';
const dayStreakText =
typeof summary.startedDays === 'number'
? `已坚持 ${summary.startedDays}`
: medication
? `开始于 ${dayjs(medication.startDate).format('YYYY年M月D日')}`
: '暂无开始日期';
const handleDeleteMedication = useCallback(async () => {
if (!medication || deleteLoading) {
console.log('[MEDICATION_DETAIL] Delete aborted', {
hasMedication: !!medication,
deleteLoading
});
return;
}
console.log('[MEDICATION_DETAIL] Starting delete operation for medication', medication.id);
try {
setDeleteLoading(true);
setDeleteSheetVisible(false); // 立即关闭确认对话框
await dispatch(deleteMedicationAction(medication.id)).unwrap();
console.log('[MEDICATION_DETAIL] Delete operation successful, navigating back');
router.back();
} catch (err) {
console.error('删除药品失败', err);
Alert.alert('删除失败', '移除该药品时出现问题,请稍后再试。');
} finally {
setDeleteLoading(false);
}
}, [deleteLoading, dispatch, medication, router]);
const handleImagePreview = useCallback(() => {
if (medication?.photoUrl) {
setShowImagePreview(true);
}
}, [medication?.photoUrl]);
const handleStartDatePress = useCallback(() => {
Alert.alert('开始日期', `开始服药日期:${startDateLabel}`);
}, [startDateLabel]);
const handleTimePress = useCallback(() => {
Alert.alert('服药时间', `设置的时间:${reminderTimes}`);
}, [reminderTimes]);
const handleDosagePress = useCallback(() => {
if (!medication) return;
setDosagePickerVisible(true);
}, [medication]);
const handleFormPress = useCallback(() => {
if (!medication) return;
setFormPickerVisible(true);
}, [medication]);
const handleFrequencyPress = useCallback(() => {
if (!medication) return;
setFrequencyPickerVisible(true);
}, [medication]);
const confirmDosagePicker = useCallback(async () => {
if (!medication || updatePending) return;
setDosagePickerVisible(false);
// 如果值没有变化,不需要更新
if (dosageValuePicker === medication.dosageValue && dosageUnitPicker === medication.dosageUnit) {
return;
}
try {
setUpdatePending(true);
const updated = await dispatch(
updateMedicationAction({
id: medication.id,
dosageValue: dosageValuePicker,
dosageUnit: dosageUnitPicker,
})
).unwrap();
setMedication(updated);
} catch (err) {
console.error('更新剂量失败', err);
Alert.alert('更新失败', '更新剂量时出现问题,请稍后重试。');
} finally {
setUpdatePending(false);
}
}, [dispatch, dosageUnitPicker, dosageValuePicker, medication, updatePending]);
const confirmFormPicker = useCallback(async () => {
if (!medication || updatePending) return;
setFormPickerVisible(false);
// 如果值没有变化,不需要更新
if (formPicker === medication.form) {
return;
}
try {
setUpdatePending(true);
const updated = await dispatch(
updateMedicationAction({
id: medication.id,
form: formPicker,
})
).unwrap();
setMedication(updated);
} catch (err) {
console.error('更新剂型失败', err);
Alert.alert('更新失败', '更新剂型时出现问题,请稍后重试。');
} finally {
setUpdatePending(false);
}
}, [dispatch, formPicker, medication, updatePending]);
const confirmFrequencyPicker = useCallback(async () => {
if (!medication || updatePending) return;
setFrequencyPickerVisible(false);
// 检查频率和时间是否都没有变化
const frequencyChanged = repeatPatternPicker !== medication.repeatPattern || timesPerDayPicker !== medication.timesPerDay;
const timesChanged = JSON.stringify(medicationTimesPicker) !== JSON.stringify(medication.medicationTimes);
if (!frequencyChanged && !timesChanged) {
return;
}
try {
setUpdatePending(true);
const updated = await dispatch(
updateMedicationAction({
id: medication.id,
repeatPattern: repeatPatternPicker,
timesPerDay: timesPerDayPicker,
medicationTimes: medicationTimesPicker, // 同时更新提醒时间
})
).unwrap();
setMedication(updated);
} catch (err) {
console.error('更新频率失败', err);
Alert.alert('更新失败', '更新服药频率时出现问题,请稍后重试。');
} finally {
setUpdatePending(false);
}
}, [dispatch, repeatPatternPicker, timesPerDayPicker, medicationTimesPicker, medication, updatePending]);
// 打开时间选择器
const openTimePicker = useCallback(
(index?: number) => {
try {
if (typeof index === 'number') {
if (index >= 0 && index < medicationTimesPicker.length) {
setEditingTimeIndex(index);
setTimePickerDate(createDateFromTime(medicationTimesPicker[index]));
} else {
console.error('[MEDICATION] Invalid time index:', index);
return;
}
} else {
setEditingTimeIndex(null);
setTimePickerDate(createDateFromTime(getDefaultTimeByIndex(medicationTimesPicker.length)));
}
setTimePickerVisible(true);
} catch (error) {
console.error('[MEDICATION] Error in openTimePicker:', error);
}
},
[medicationTimesPicker, createDateFromTime, getDefaultTimeByIndex]
);
// 确认时间选择
const confirmTime = useCallback(
(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);
setMedicationTimesPicker((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, formatTime]
);
// 删除时间
const removeTime = useCallback((index: number) => {
setMedicationTimesPicker((prev) => {
if (prev.length === 1) {
return prev; // 至少保留一个时间
}
return prev.filter((_, idx) => idx !== index);
});
// 同时更新 timesPerDayPicker
setTimesPerDayPicker((prev) => Math.max(1, prev - 1));
}, []);
// 添加时间
const addTime = useCallback(() => {
openTimePicker();
// 同时更新 timesPerDayPicker
setTimesPerDayPicker((prev) => prev + 1);
}, [openTimePicker]);
if (!medicationId) {
return (
<View style={[styles.container, { backgroundColor: colors.pageBackgroundEmphasis }]}>
<HeaderBar title="药品详情" variant="minimal" />
<View style={[styles.centered, { paddingTop: insets.top + 72, paddingHorizontal: 24 }]}>
<ThemedText style={styles.emptyTitle}></ThemedText>
<ThemedText style={styles.emptySubtitle}></ThemedText>
</View>
</View>
);
}
const isLoadingState = loading && !medication;
const contentBottomPadding = Math.max(insets.bottom, 16) + 140;
return (
<View style={[styles.container, { backgroundColor: colors.pageBackgroundEmphasis}]}>
<HeaderBar title="药品详情" variant="minimal" transparent />
{isLoadingState ? (
<View style={[styles.centered, { paddingTop: insets.top + 48 }]}>
<ActivityIndicator color={colors.primary} />
<Text style={[styles.loadingText, { color: colors.textSecondary }]}>...</Text>
</View>
) : error ? (
<View style={[styles.centered, { paddingHorizontal: 24, paddingTop: insets.top + 72 }]}>
<ThemedText style={styles.emptyTitle}>{error}</ThemedText>
<ThemedText style={[styles.emptySubtitle, { color: colors.textMuted }]}>
</ThemedText>
</View>
) : medication ? (
<ScrollView
contentContainerStyle={[
styles.content,
{
paddingTop: insets.top + 48,
paddingBottom: contentBottomPadding,
},
]}
showsVerticalScrollIndicator={false}
>
<View style={[styles.heroCard, { backgroundColor: colors.surface }]}>
<View style={styles.heroInfo}>
<TouchableOpacity
style={styles.heroImageWrapper}
onPress={handleImagePreview}
activeOpacity={0.8}
disabled={!medication.photoUrl}
>
<Image
source={medication.photoUrl ? { uri: medication.photoUrl } : DEFAULT_IMAGE}
style={styles.heroImage}
contentFit="cover"
/>
{medication.photoUrl && (
<View style={styles.imagePreviewHint}>
<Ionicons name="expand-outline" size={14} color="#FFF" />
</View>
)}
</TouchableOpacity>
<View>
<Text style={[styles.heroTitle, { color: colors.text }]}>{medication.name}</Text>
<Text style={[styles.heroMeta, { color: colors.textSecondary }]}>
{dosageLabel} · {formLabel}
</Text>
</View>
</View>
<View style={styles.heroToggle}>
<Switch
value={medication.isActive}
onValueChange={handleToggleMedication}
disabled={updatePending}
trackColor={{ false: '#D9D9D9', true: colors.primary }}
thumbColor="#fff"
ios_backgroundColor="#D9D9D9"
/>
</View>
</View>
<Section title="服药计划" color={colors.text}>
<View style={styles.row}>
<InfoCard
label="开始日期"
value={startDateLabel}
icon="calendar-outline"
colors={colors}
clickable={false}
onPress={handleStartDatePress}
/>
<InfoCard
label="时间"
value={reminderTimes}
icon="time-outline"
colors={colors}
clickable={false}
onPress={handleTimePress}
/>
</View>
<TouchableOpacity
style={[styles.fullCard, { backgroundColor: colors.surface }]}
onPress={handleFrequencyPress}
activeOpacity={0.7}
>
<View style={styles.fullCardLeading}>
<Ionicons name="repeat-outline" size={18} color={colors.primary} />
<Text style={[styles.fullCardLabel, { color: colors.text }]}></Text>
</View>
<View style={styles.fullCardTrailing}>
<Text style={[styles.fullCardValue, { color: colors.text }]}>{frequencyLabel}</Text>
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
</View>
</TouchableOpacity>
</Section>
<Section title="剂量与形式" color={colors.text}>
<View style={styles.row}>
<InfoCard
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>
</Section>
<Section title="备注" color={colors.text}>
<TouchableOpacity
style={[styles.noteCard, { backgroundColor: colors.surface }]}
activeOpacity={0.92}
onPress={handleOpenNoteModal}
>
<Ionicons name="document-text-outline" size={20} color={colors.primary} />
<View style={styles.noteBody}>
<Text style={[styles.noteLabel, { color: colors.text }]}></Text>
<Text
style={[
styles.noteValue,
{ color: medication.note ? colors.text : colors.textMuted },
]}
>
{noteText}
</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
</TouchableOpacity>
</Section>
<Section title="服药概览" color={colors.text}>
<View style={[styles.summaryCard, { backgroundColor: colors.surface }]}>
<View style={styles.summaryIcon}>
<Ionicons name="tablet-portrait-outline" size={22} color={colors.primary} />
</View>
<View style={styles.summaryBody}>
<Text style={[styles.summaryHighlight, { color: colors.text }]}>
{summaryLoading ? '统计中...' : `累计服药 ${summary.takenCount}`}
</Text>
<Text style={[styles.summaryMeta, { color: colors.textSecondary }]}>
{summaryLoading ? '正在计算坚持天数' : dayStreakText}
</Text>
</View>
</View>
</Section>
</ScrollView>
) : null}
{medication ? (
<View
style={[
styles.footerBar,
{
paddingBottom: Math.max(insets.bottom, 18),
backgroundColor: colors.pageBackgroundEmphasis,
},
]}
>
<TouchableOpacity
activeOpacity={0.9}
onPress={() => setDeleteSheetVisible(true)}
>
{isLiquidGlassAvailable() ? (
<GlassView
style={styles.deleteButton}
glassEffectStyle="regular"
tintColor="rgba(239, 68, 68, 0.8)"
isInteractive={true}
>
<Ionicons name='trash-outline' size={18} color='#fff' />
<Text style={styles.deleteButtonText}></Text>
</GlassView>
) : (
<View style={[styles.deleteButton, styles.fallbackDeleteButton]}>
<Ionicons name='trash-outline' size={18} color='#fff' />
<Text style={styles.deleteButtonText}></Text>
</View>
)}
</TouchableOpacity>
</View>
) : null}
<Modal
transparent
animationType="fade"
visible={noteModalVisible}
onRequestClose={closeNoteModal}
>
<View style={styles.modalOverlay}>
<TouchableOpacity style={styles.modalBackdrop} activeOpacity={1} onPress={closeNoteModal} />
<View
style={[
styles.modalContainer,
{ paddingBottom: Math.max(keyboardHeight, insets.bottom) + 12 },
]}
>
<View style={[styles.modalCard, { backgroundColor: colors.surface }]}>
<View style={styles.modalHandle} />
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: colors.text }]}></Text>
<TouchableOpacity onPress={closeNoteModal} hitSlop={12}>
<Ionicons name="close" size={20} color={colors.textSecondary} />
</TouchableOpacity>
</View>
<View
style={[
styles.noteEditorWrapper,
{
borderColor: dictationActive ? colors.primary : `${colors.border}80`,
backgroundColor: colors.surface,
},
]}
>
<TextInput
multiline
numberOfLines={6}
value={noteDraft}
onChangeText={setNoteDraft}
placeholder="记录注意事项、医生叮嘱或自定义提醒"
placeholderTextColor={colors.textMuted}
style={[styles.noteEditorInput, { color: colors.text }]}
textAlignVertical="center"
/>
{isDictationSupported && (
<TouchableOpacity
style={[
styles.voiceButton,
{
backgroundColor: dictationActive ? colors.primary : 'transparent',
borderColor: dictationActive ? colors.primary : `${colors.border}80`,
},
]}
onPress={handleDictationPress}
activeOpacity={0.85}
disabled={dictationLoading}
>
{dictationLoading ? (
<ActivityIndicator size="small" color={dictationActive ? colors.onPrimary : colors.primary} />
) : (
<Ionicons
name={dictationActive ? 'mic' : 'mic-outline'}
size={18}
color={dictationActive ? colors.onPrimary : colors.textSecondary}
/>
)}
</TouchableOpacity>
)}
</View>
{!isDictationSupported && (
<Text style={[styles.voiceHint, { color: colors.textMuted }]}>
</Text>
)}
<View style={styles.modalActionContainer}>
<TouchableOpacity
style={[
styles.modalActionPrimary,
{
backgroundColor: colors.primary,
shadowColor: colors.primary,
},
]}
onPress={handleSaveNote}
activeOpacity={0.9}
disabled={noteSaving}
>
{noteSaving ? (
<ActivityIndicator color={colors.onPrimary} size="small" />
) : (
<Text style={[styles.modalActionPrimaryText, { color: colors.onPrimary }]}></Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
</View>
</Modal>
<Modal
visible={dosagePickerVisible}
transparent
animationType="fade"
onRequestClose={() => setDosagePickerVisible(false)}
>
<Pressable
style={styles.pickerBackdrop}
onPress={() => setDosagePickerVisible(false)}
/>
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
</ThemedText>
<View style={styles.pickerRow}>
<View style={styles.pickerColumn}>
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
</ThemedText>
<Picker
selectedValue={dosageValuePicker}
onValueChange={(value) => setDosageValuePicker(Number(value))}
itemStyle={styles.pickerItem}
style={styles.picker}
>
{DOSAGE_VALUES.map((value) => (
<Picker.Item
key={value}
label={String(value)}
value={value}
/>
))}
</Picker>
</View>
<View style={styles.pickerColumn}>
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
</ThemedText>
<Picker
selectedValue={dosageUnitPicker}
onValueChange={(value) => setDosageUnitPicker(String(value))}
itemStyle={styles.pickerItem}
style={styles.picker}
>
{DOSAGE_UNITS.map((unit) => (
<Picker.Item
key={unit}
label={unit}
value={unit}
/>
))}
</Picker>
</View>
</View>
<View style={styles.pickerActions}>
<Pressable
onPress={() => setDosagePickerVisible(false)}
style={[styles.pickerBtn, { borderColor: colors.border }]}
>
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>
</ThemedText>
</Pressable>
<Pressable
onPress={confirmDosagePicker}
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
>
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
</ThemedText>
</Pressable>
</View>
</View>
</Modal>
<Modal
visible={formPickerVisible}
transparent
animationType="fade"
onRequestClose={() => setFormPickerVisible(false)}
>
<Pressable
style={styles.pickerBackdrop}
onPress={() => setFormPickerVisible(false)}
/>
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
</ThemedText>
<Picker
selectedValue={formPicker}
onValueChange={(value) => setFormPicker(value as MedicationForm)}
itemStyle={styles.pickerItem}
style={styles.picker}
>
{FORM_OPTIONS.map((option) => (
<Picker.Item
key={option.id}
label={option.label}
value={option.id}
/>
))}
</Picker>
<View style={styles.pickerActions}>
<Pressable
onPress={() => setFormPickerVisible(false)}
style={[styles.pickerBtn, { borderColor: colors.border }]}
>
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>
</ThemedText>
</Pressable>
<Pressable
onPress={confirmFormPicker}
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
>
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
</ThemedText>
</Pressable>
</View>
</View>
</Modal>
<Modal
visible={frequencyPickerVisible}
transparent
animationType="fade"
onRequestClose={() => setFrequencyPickerVisible(false)}
>
<Pressable
style={styles.pickerBackdrop}
onPress={() => setFrequencyPickerVisible(false)}
/>
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
</ThemedText>
<View style={styles.pickerRow}>
<View style={styles.pickerColumn}>
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
</ThemedText>
<Picker
selectedValue={repeatPatternPicker}
onValueChange={(value) => setRepeatPatternPicker(value as RepeatPattern)}
itemStyle={styles.pickerItem}
style={styles.picker}
>
<Picker.Item label="每日" value="daily" />
{/* <Picker.Item label="每周" value="weekly" />
<Picker.Item label="自定义" value="custom" /> */}
</Picker>
</View>
<View style={styles.pickerColumn}>
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
</ThemedText>
<Picker
selectedValue={timesPerDayPicker}
onValueChange={(value) => setTimesPerDayPicker(Number(value))}
itemStyle={styles.pickerItem}
style={styles.picker}
>
{TIMES_PER_DAY_OPTIONS.map((times) => (
<Picker.Item
key={times}
label={`${times}`}
value={times}
/>
))}
</Picker>
</View>
</View>
{/* 提醒时间列表 */}
<View style={styles.timeListSection}>
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary, marginBottom: 8 }]}>
</ThemedText>
<ScrollView style={styles.timeListScroll} showsVerticalScrollIndicator={false}>
{medicationTimesPicker.map((time, index) => (
<View
key={`${time}-${index}`}
style={[
styles.timeItemInPicker,
{
borderColor: `${colors.border}80`,
backgroundColor: colors.pageBackgroundEmphasis,
},
]}
>
<TouchableOpacity style={styles.timeValue} onPress={() => openTimePicker(index)}>
<Ionicons name="time" size={16} color={colors.primary} />
<ThemedText style={[styles.timeTextSmall, { color: colors.text }]}>{time}</ThemedText>
</TouchableOpacity>
<Pressable onPress={() => removeTime(index)} disabled={medicationTimesPicker.length === 1} hitSlop={12}>
<Ionicons
name="close-circle"
size={16}
color={medicationTimesPicker.length === 1 ? `${colors.border}80` : colors.textSecondary}
/>
</Pressable>
</View>
))}
<TouchableOpacity
style={[styles.addTimeButtonSmall, { borderColor: colors.primary }]}
onPress={addTime}
>
<Ionicons name="add" size={14} color={colors.primary} />
<ThemedText style={[styles.addTimeLabelSmall, { color: colors.primary }]}></ThemedText>
</TouchableOpacity>
</ScrollView>
</View>
<View style={styles.pickerActions}>
<Pressable
onPress={() => setFrequencyPickerVisible(false)}
style={[styles.pickerBtn, { borderColor: colors.border }]}
>
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>
</ThemedText>
</Pressable>
<Pressable
onPress={confirmFrequencyPicker}
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
>
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
</ThemedText>
</Pressable>
</View>
</View>
</Modal>
{/* 时间选择器 Modal */}
<Modal
visible={timePickerVisible}
transparent
animationType="fade"
onRequestClose={() => {
setTimePickerVisible(false);
setEditingTimeIndex(null);
}}
>
<Pressable
style={styles.pickerBackdrop}
onPress={() => {
setTimePickerVisible(false);
setEditingTimeIndex(null);
}}
/>
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
{editingTimeIndex !== null ? '修改提醒时间' : '添加提醒时间'}
</ThemedText>
<DateTimePicker
value={timePickerDate}
mode="time"
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
onChange={(event, date) => {
if (Platform.OS === 'ios') {
if (date) setTimePickerDate(date);
} else {
if (event.type === 'set' && date) {
confirmTime(date);
} else {
setTimePickerVisible(false);
setEditingTimeIndex(null);
}
}
}}
/>
{Platform.OS === 'ios' && (
<View style={styles.pickerActions}>
<Pressable
onPress={() => {
setTimePickerVisible(false);
setEditingTimeIndex(null);
}}
style={[styles.pickerBtn, { borderColor: colors.border }]}
>
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}></ThemedText>
</Pressable>
<Pressable
onPress={() => confirmTime(timePickerDate)}
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
>
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}></ThemedText>
</Pressable>
</View>
)}
</View>
</Modal>
{medication ? (
<ConfirmationSheet
visible={deleteSheetVisible}
onClose={() => setDeleteSheetVisible(false)}
onConfirm={handleDeleteMedication}
title={`删除 ${medication.name}`}
description="删除后将清除与该药品相关的提醒与历史记录,且无法恢复。"
confirmText="删除"
cancelText="取消"
destructive
loading={deleteLoading}
/>
) : null}
{/* 图片预览 */}
{medication?.photoUrl && (
<ImageViewing
images={[{ uri: medication.photoUrl }]}
imageIndex={0}
visible={showImagePreview}
onRequestClose={() => setShowImagePreview(false)}
swipeToCloseEnabled={true}
doubleTapToZoomEnabled={true}
HeaderComponent={() => (
<View style={styles.imageViewerHeader}>
<Text style={styles.imageViewerHeaderText}>
{medication.name}
</Text>
</View>
)}
FooterComponent={() => (
<View style={styles.imageViewerFooter}>
<TouchableOpacity
style={styles.imageViewerFooterButton}
onPress={() => setShowImagePreview(false)}
>
<Text style={styles.imageViewerFooterButtonText}></Text>
</TouchableOpacity>
</View>
)}
/>
)}
</View>
);
}
const Section = ({
title,
children,
color,
}: {
title: string;
children: React.ReactNode;
color: string;
}) => {
return (
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color }]}>{title}</Text>
{children}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
position: 'relative',
},
content: {
paddingHorizontal: 20,
gap: 24,
},
centered: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
},
loadingText: {
marginTop: 8,
fontSize: 14,
},
emptyTitle: {
fontSize: 18,
fontWeight: '600',
textAlign: 'center',
},
emptySubtitle: {
fontSize: 14,
textAlign: 'center',
},
heroCard: {
borderRadius: 28,
padding: 20,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 12,
shadowOffset: { width: 0, height: 8 },
elevation: 3,
gap: 12,
},
heroInfo: {
flexDirection: 'row',
alignItems: 'center',
gap: 14,
flex: 1,
},
heroImageWrapper: {
width: 64,
height: 64,
borderRadius: 20,
backgroundColor: '#F2F2F2',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
},
heroImage: {
width: '60%',
height: '60%',
borderRadius: '20%'
},
heroTitle: {
fontSize: 20,
fontWeight: '700',
},
heroMeta: {
marginTop: 4,
fontSize: 13,
fontWeight: '500',
},
heroToggle: {
alignItems: 'flex-end',
gap: 6,
},
section: {
gap: 12,
},
sectionTitle: {
fontSize: 18,
fontWeight: '700',
},
row: {
flexDirection: 'row',
gap: 12,
},
fullCard: {
borderRadius: 22,
padding: 18,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
fullCardLeading: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
fullCardLabel: {
fontSize: 15,
fontWeight: '600',
},
fullCardTrailing: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
fullCardValue: {
fontSize: 16,
fontWeight: '600',
},
noteCard: {
flexDirection: 'row',
alignItems: 'center',
gap: 14,
borderRadius: 24,
paddingHorizontal: 18,
paddingVertical: 16,
},
noteBody: {
flex: 1,
gap: 4,
},
noteLabel: {
fontSize: 14,
fontWeight: '600',
},
noteValue: {
fontSize: 14,
lineHeight: 20,
},
summaryCard: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 24,
padding: 18,
gap: 16,
},
summaryIcon: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#EEF1FF',
alignItems: 'center',
justifyContent: 'center',
},
summaryBody: {
flex: 1,
gap: 4,
},
summaryHighlight: {
fontSize: 16,
fontWeight: '700',
},
summaryMeta: {
fontSize: 14,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.35)',
justifyContent: 'flex-end',
},
modalBackdrop: {
flex: 1,
},
modalContainer: {
width: '100%',
paddingHorizontal: 20,
},
modalCard: {
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: 32,
gap: 16,
},
modalHandle: {
width: 60,
height: 5,
borderRadius: 2.5,
backgroundColor: 'rgba(0,0,0,0.12)',
alignSelf: 'center',
marginBottom: 12,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
modalTitle: {
fontSize: 18,
fontWeight: '700',
},
noteEditorWrapper: {
borderWidth: 1,
borderRadius: 24,
paddingHorizontal: 18,
paddingVertical: 24,
minHeight: 120,
paddingRight: 70,
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 12,
shadowOffset: { width: 0, height: 6 },
elevation: 3,
},
noteEditorInput: {
minHeight: 50,
fontSize: 15,
lineHeight: 22,
},
voiceButton: {
position: 'absolute',
right: 16,
top: 16,
width: 40,
height: 40,
borderRadius: 20,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
voiceHint: {
fontSize: 12,
},
modalActionGhostText: {
fontSize: 16,
fontWeight: '600',
},
modalActionPrimary: {
borderRadius: 22,
height: 52,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 8,
shadowOpacity: 0.25,
shadowRadius: 12,
shadowOffset: { width: 0, height: 8 },
elevation: 4,
},
modalActionPrimaryText: {
fontSize: 17,
fontWeight: '700',
},
modalActionContainer: {
marginTop: 8,
},
footerBar: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
paddingHorizontal: 20,
paddingTop: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: 'rgba(15,23,42,0.06)',
},
deleteButton: {
height: 56,
borderRadius: 24,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
overflow: 'hidden', // 保证玻璃边界圆角效果
},
fallbackDeleteButton: {
backgroundColor: '#EF4444',
shadowColor: 'rgba(239,68,68,0.4)',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 1,
shadowRadius: 20,
elevation: 6,
},
deleteButtonText: {
fontSize: 17,
fontWeight: '700',
color: '#fff',
},
// Picker 相关样式
pickerBackdrop: {
flex: 1,
backgroundColor: 'rgba(15, 23, 42, 0.4)',
},
pickerSheet: {
position: 'absolute',
left: 20,
right: 20,
bottom: 40,
borderRadius: 24,
padding: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.2,
shadowRadius: 20,
elevation: 8,
},
pickerTitle: {
fontSize: 20,
fontWeight: '700',
marginBottom: 20,
textAlign: 'center',
},
pickerRow: {
flexDirection: 'row',
gap: 16,
marginBottom: 20,
},
pickerColumn: {
flex: 1,
gap: 8,
},
pickerLabel: {
fontSize: 14,
fontWeight: '600',
textAlign: 'center',
},
picker: {
width: '100%',
height: 150,
},
pickerItem: {
fontSize: 18,
height: 150,
},
pickerActions: {
flexDirection: 'row',
gap: 12,
marginTop: 16,
},
pickerBtn: {
flex: 1,
paddingVertical: 14,
borderRadius: 16,
alignItems: 'center',
borderWidth: 1,
},
pickerBtnPrimary: {
borderWidth: 0,
},
pickerBtnText: {
fontSize: 16,
fontWeight: '600',
},
// 时间列表相关样式
timeListSection: {
gap: 12,
marginTop: 16,
},
timeListScroll: {
maxHeight: 200,
},
timeItemInPicker: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 14,
paddingVertical: 10,
marginBottom: 8,
},
timeValue: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
timeTextSmall: {
fontSize: 16,
fontWeight: '600',
},
addTimeButtonSmall: {
borderWidth: 1,
borderRadius: 12,
paddingVertical: 10,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
},
addTimeLabelSmall: {
fontSize: 13,
fontWeight: '600',
},
imagePreviewHint: {
position: 'absolute',
top: 4,
right: 4,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderRadius: 10,
padding: 4,
},
// ImageViewing 组件样式
imageViewerHeader: {
position: 'absolute',
top: 60,
left: 20,
right: 20,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 12,
zIndex: 1,
},
imageViewerHeaderText: {
color: '#FFF',
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
},
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',
},
});