- 新增底部标签栏配置页面,支持切换标签显示/隐藏和恢复默认设置 - 实现已服用药物的堆叠卡片展示,优化药物列表视觉层次 - 集成Redux状态管理底部标签栏配置,支持本地持久化 - 优化个人中心页面背景渐变效果,移除装饰性圆圈元素 - 更新启动页和应用图标为新的品牌视觉 - 药物详情页AI分析加载动画替换为Lottie动画 - 调整药物卡片圆角半径提升视觉一致性 - 新增多语言支持(中英文)用于标签栏配置界面 主要改进: 1. 用户可以自定义底部导航栏显示内容 2. 已完成的药物以堆叠形式展示,节省空间 3. 配置数据通过AsyncStorage持久化保存 4. 支持默认配置恢复功能
2687 lines
84 KiB
TypeScript
2687 lines
84 KiB
TypeScript
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_OPTIONS } from '@/constants/Medication';
|
||
import { ROUTES } from '@/constants/Routes';
|
||
import { useMembershipModal } from '@/contexts/MembershipModalContext';
|
||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||
import { useI18n } from '@/hooks/useI18n';
|
||
import { useVipService } from '@/hooks/useVipService';
|
||
import { medicationNotificationService } from '@/services/medicationNotifications';
|
||
import {
|
||
analyzeMedicationV2,
|
||
getMedicationById,
|
||
getMedicationRecords,
|
||
} from '@/services/medications';
|
||
import {
|
||
deleteMedicationAction,
|
||
fetchMedications,
|
||
selectMedications,
|
||
updateMedicationAction,
|
||
} from '@/store/medicationsSlice';
|
||
import type { Medication, MedicationAiAnalysisV2, MedicationForm } from '@/types/medication';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
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 * as ImagePicker from 'expo-image-picker';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||
import LottieView from 'lottie-react-native';
|
||
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 { t } = useI18n();
|
||
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 { ensureLoggedIn } = useAuthGuard();
|
||
const { openMembershipModal } = useMembershipModal();
|
||
const { checkServiceAccess } = useVipService();
|
||
const { upload, uploading } = useCosUpload({ prefix: 'images/medications' });
|
||
|
||
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 [nameModalVisible, setNameModalVisible] = useState(false);
|
||
const [nameDraft, setNameDraft] = useState(medicationFromStore?.name ?? '');
|
||
const [nameSaving, setNameSaving] = 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 [deactivateSheetVisible, setDeactivateSheetVisible] = useState(false);
|
||
const [deactivateLoading, setDeactivateLoading] = useState(false);
|
||
const [showImagePreview, setShowImagePreview] = useState(false);
|
||
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
||
|
||
// AI 分析相关状态
|
||
const [aiAnalysisLoading, setAiAnalysisLoading] = useState(false);
|
||
const [aiAnalysisResult, setAiAnalysisResult] = useState<MedicationAiAnalysisV2 | null>(null);
|
||
const [aiAnalysisError, setAiAnalysisError] = useState<string | null>(null);
|
||
const [aiAnalysisLocked, setAiAnalysisLocked] = 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'
|
||
);
|
||
|
||
// ScrollView 引用,用于滚动到底部
|
||
const scrollViewRef = React.useRef<ScrollView>(null);
|
||
|
||
useEffect(() => {
|
||
if (!medicationFromStore) {
|
||
dispatch(fetchMedications());
|
||
}
|
||
}, [dispatch, medicationFromStore]);
|
||
|
||
useEffect(() => {
|
||
setAiAnalysisError(null);
|
||
}, [medicationId]);
|
||
|
||
useEffect(() => {
|
||
if (medicationFromStore) {
|
||
setMedication(medicationFromStore);
|
||
setLoading(false);
|
||
setAiAnalysisResult(null);
|
||
// 如果服务端返回了 AI 分析结果,自动展示
|
||
if (medicationFromStore.aiAnalysis) {
|
||
try {
|
||
const parsed = JSON.parse(medicationFromStore.aiAnalysis);
|
||
if (parsed && typeof parsed === 'object') {
|
||
setAiAnalysisResult(parsed as MedicationAiAnalysisV2);
|
||
}
|
||
} catch {
|
||
// ignore legacy markdown
|
||
}
|
||
}
|
||
}
|
||
}, [medicationFromStore]);
|
||
|
||
useEffect(() => {
|
||
// 同步剂量选择器和剂型选择器的默认值
|
||
if (medication) {
|
||
setDosageValuePicker(medication.dosageValue);
|
||
setDosageUnitPicker(medication.dosageUnit);
|
||
setFormPicker(medication.form);
|
||
}
|
||
}, [medication?.dosageValue, medication?.dosageUnit, medication?.form]);
|
||
|
||
useEffect(() => {
|
||
setNoteDraft(medication?.note ?? '');
|
||
}, [medication?.note]);
|
||
useEffect(() => {
|
||
setNameDraft(medication?.name ?? '');
|
||
}, [medication?.name]);
|
||
|
||
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);
|
||
// 如果服务端返回了 AI 分析结果,自动展示
|
||
setAiAnalysisResult(null);
|
||
if (data.aiAnalysis) {
|
||
try {
|
||
const parsed = JSON.parse(data.aiAnalysis);
|
||
if (parsed && typeof parsed === 'object') {
|
||
setAiAnalysisResult(parsed as MedicationAiAnalysisV2);
|
||
}
|
||
} catch {
|
||
// ignore legacy markdown
|
||
}
|
||
}
|
||
})
|
||
.catch((err) => {
|
||
if (abortController.signal.aborted) return;
|
||
console.error('加载药品详情失败', err);
|
||
console.log('[MEDICATION_DETAIL] API call failed for medication', medicationId, err);
|
||
if (isMounted) {
|
||
setError(t('medications.detail.error.title'));
|
||
}
|
||
})
|
||
.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(t('medications.add.note.voiceError'), t('medications.add.note.voiceErrorMessage'));
|
||
};
|
||
|
||
return () => {
|
||
Voice.destroy()
|
||
.then(() => {
|
||
Voice.removeAllListeners();
|
||
})
|
||
.catch(() => {});
|
||
};
|
||
}, [appendDictationResult, isDictationSupported, noteModalVisible]);
|
||
|
||
useEffect(() => {
|
||
// 统一处理名字编辑弹窗和备注弹窗的键盘监听
|
||
if (!noteModalVisible && !nameModalVisible) {
|
||
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, nameModalVisible]);
|
||
|
||
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(t('medications.add.note.voiceStartError'), t('medications.add.note.voiceStartErrorMessage'));
|
||
}
|
||
}, [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;
|
||
|
||
// 如果是关闭激活状态,显示确认弹窗
|
||
if (!nextValue) {
|
||
setDeactivateSheetVisible(true);
|
||
return;
|
||
}
|
||
|
||
// 如果是开启激活状态,直接执行
|
||
try {
|
||
setUpdatePending(true);
|
||
const updated = await dispatch(
|
||
updateMedicationAction({
|
||
id: medication.id,
|
||
isActive: nextValue,
|
||
})
|
||
).unwrap();
|
||
setMedication(updated);
|
||
|
||
// 重新安排药品通知
|
||
try {
|
||
if (nextValue) {
|
||
// 如果激活了药品,安排通知
|
||
await medicationNotificationService.scheduleMedicationNotifications(updated);
|
||
}
|
||
} catch (error) {
|
||
console.error('[MEDICATION] 处理药品通知失败:', error);
|
||
// 不影响药品状态切换的成功流程,只记录错误
|
||
}
|
||
} catch (err) {
|
||
console.error('切换药品状态失败', err);
|
||
Alert.alert(t('medications.detail.toggleError.title'), t('medications.detail.toggleError.message'));
|
||
} finally {
|
||
setUpdatePending(false);
|
||
}
|
||
};
|
||
|
||
const handleDeactivateMedication = useCallback(async () => {
|
||
if (!medication || deactivateLoading) return;
|
||
|
||
try {
|
||
setDeactivateLoading(true);
|
||
setDeactivateSheetVisible(false); // 立即关闭确认对话框
|
||
|
||
const updated = await dispatch(
|
||
updateMedicationAction({
|
||
id: medication.id,
|
||
isActive: false,
|
||
})
|
||
).unwrap();
|
||
setMedication(updated);
|
||
|
||
// 取消该药品的通知
|
||
try {
|
||
await medicationNotificationService.cancelMedicationNotifications(updated.id);
|
||
} catch (error) {
|
||
console.error('[MEDICATION] 取消药品通知失败:', error);
|
||
// 不影响药品状态切换的成功流程,只记录错误
|
||
}
|
||
} catch (error) {
|
||
console.error('停用药物失败', error);
|
||
Alert.alert(t('medications.detail.deactivate.error.title'), t('medications.detail.deactivate.error.message'));
|
||
} finally {
|
||
setDeactivateLoading(false);
|
||
}
|
||
}, [dispatch, medication, deactivateLoading]);
|
||
|
||
const formLabel = medication ? t(`medications.manage.formLabels.${medication.form}`) : '';
|
||
const dosageLabel = medication ? `${medication.dosageValue} ${medication.dosageUnit}` : '--';
|
||
const startDateLabel = medication
|
||
? dayjs(medication.startDate).format('YYYY年M月D日')
|
||
: '--';
|
||
|
||
// 计算服药周期显示
|
||
const medicationPeriodLabel = useMemo(() => {
|
||
if (!medication) return '--';
|
||
|
||
const startDate = dayjs(medication.startDate).format('YYYY年M月D日');
|
||
|
||
if (medication.endDate) {
|
||
// 有结束日期,显示开始日期到结束日期
|
||
const endDate = dayjs(medication.endDate).format('YYYY年M月D日');
|
||
return `${startDate} - ${endDate}`;
|
||
} else {
|
||
// 没有结束日期,显示长期
|
||
return `${startDate} - ${t('medications.detail.plan.longTerm')}`;
|
||
}
|
||
}, [medication, t]);
|
||
|
||
const reminderTimes = medication?.medicationTimes?.length
|
||
? medication.medicationTimes.join('、')
|
||
: t('medications.manage.reminderNotSet');
|
||
const frequencyLabel = useMemo(() => {
|
||
if (!medication) return '--';
|
||
switch (medication.repeatPattern) {
|
||
case 'daily':
|
||
return `${t('medications.manage.frequency.daily')} ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`;
|
||
case 'weekly':
|
||
return `${t('medications.manage.frequency.weekly')} ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`;
|
||
default:
|
||
return `${t('medications.manage.frequency.custom')} · ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`;
|
||
}
|
||
}, [medication, t]);
|
||
const serviceInfo = useMemo(() => checkServiceAccess(), [checkServiceAccess]);
|
||
const hasAiAnalysis = Boolean(aiAnalysisResult);
|
||
const aiActionLabel = aiAnalysisLoading
|
||
? t('medications.detail.aiAnalysis.analyzingButton')
|
||
: hasAiAnalysis
|
||
? '重新分析'
|
||
: '获取 AI 分析';
|
||
|
||
const handleOpenNoteModal = useCallback(() => {
|
||
setNoteDraft(medication?.note ?? '');
|
||
setNoteModalVisible(true);
|
||
}, [medication?.note]);
|
||
|
||
const handleOpenNameModal = useCallback(() => {
|
||
setNameDraft(medication?.name ?? '');
|
||
setNameModalVisible(true);
|
||
}, [medication?.name]);
|
||
|
||
const handleCloseNameModal = useCallback(() => {
|
||
setNameModalVisible(false);
|
||
}, []);
|
||
|
||
const handleNameChange = useCallback((value: string) => {
|
||
const sanitized = value.replace(/\n/g, '');
|
||
const normalized = Array.from(sanitized).slice(0, 10).join('');
|
||
setNameDraft(normalized);
|
||
}, []);
|
||
|
||
const handleSaveName = useCallback(async () => {
|
||
if (!medication || nameSaving) return;
|
||
const trimmed = nameDraft.trim();
|
||
if (!trimmed) {
|
||
Alert.alert(
|
||
t('medications.detail.nameEdit.errorTitle', { defaultValue: '提示' }),
|
||
t('medications.detail.nameEdit.errorEmpty', { defaultValue: '药物名称不能为空' })
|
||
);
|
||
return;
|
||
}
|
||
if (Array.from(trimmed).length > 10) {
|
||
Alert.alert(
|
||
t('medications.detail.nameEdit.errorTitle', { defaultValue: '提示' }),
|
||
t('medications.detail.nameEdit.errorTooLong', { defaultValue: '药物名称不能超过10个字' })
|
||
);
|
||
return;
|
||
}
|
||
setNameSaving(true);
|
||
try {
|
||
const updated = await dispatch(
|
||
updateMedicationAction({
|
||
id: medication.id,
|
||
name: trimmed,
|
||
})
|
||
).unwrap();
|
||
setMedication(updated);
|
||
setNameModalVisible(false);
|
||
} catch (err) {
|
||
console.error('更新药物名称失败', err);
|
||
Alert.alert(
|
||
t('medications.detail.nameEdit.errorTitle', { defaultValue: '提示' }),
|
||
t('medications.detail.nameEdit.saveError', { defaultValue: '名称更新失败,请稍后再试' })
|
||
);
|
||
} finally {
|
||
setNameSaving(false);
|
||
}
|
||
}, [dispatch, medication, nameDraft, nameSaving, t]);
|
||
|
||
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(t('medications.detail.note.saveError.title'), t('medications.detail.note.saveError.message'));
|
||
} finally {
|
||
setNoteSaving(false);
|
||
}
|
||
}, [closeNoteModal, dispatch, medication, noteDraft]);
|
||
|
||
useEffect(() => {
|
||
if (serviceInfo.canUseService) {
|
||
setAiAnalysisLocked(false);
|
||
}
|
||
}, [serviceInfo.canUseService]);
|
||
|
||
const noteText = medication?.note?.trim() ? medication.note : t('medications.detail.note.noNote');
|
||
const dayStreakText =
|
||
typeof summary.startedDays === 'number'
|
||
? t('medications.detail.overview.startedDays', { days: summary.startedDays })
|
||
: medication
|
||
? t('medications.detail.overview.startDate', { date: dayjs(medication.startDate).format('YYYY年M月D日') })
|
||
: t('medications.detail.overview.noStartDate');
|
||
|
||
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); // 立即关闭确认对话框
|
||
|
||
// 先取消该药品的通知
|
||
try {
|
||
await medicationNotificationService.cancelMedicationNotifications(medication.id);
|
||
} catch (error) {
|
||
console.error('[MEDICATION] 取消药品通知失败:', error);
|
||
// 不影响药品删除的成功流程,只记录错误
|
||
}
|
||
|
||
await dispatch(deleteMedicationAction(medication.id)).unwrap();
|
||
console.log('[MEDICATION_DETAIL] Delete operation successful, navigating back');
|
||
router.back();
|
||
} catch (err) {
|
||
console.error('删除药品失败', err);
|
||
Alert.alert(t('medications.detail.delete.error.title'), t('medications.detail.delete.error.message'));
|
||
} finally {
|
||
setDeleteLoading(false);
|
||
}
|
||
}, [deleteLoading, dispatch, medication, router]);
|
||
|
||
const handleImagePreview = useCallback(() => {
|
||
if (medication?.photoUrl) {
|
||
setShowImagePreview(true);
|
||
}
|
||
}, [medication?.photoUrl]);
|
||
|
||
// 处理图片选择(拍照或相册)
|
||
const handleSelectPhoto = useCallback(async () => {
|
||
if (!medication || uploading) return;
|
||
|
||
Alert.alert(
|
||
t('medications.detail.photo.selectTitle', { defaultValue: '选择图片' }),
|
||
t('medications.detail.photo.selectMessage', { defaultValue: '请选择图片来源' }),
|
||
[
|
||
{
|
||
text: t('medications.detail.photo.takePhoto', { defaultValue: '拍照' }),
|
||
onPress: async () => {
|
||
try {
|
||
const permission = await ImagePicker.requestCameraPermissionsAsync();
|
||
if (permission.status !== 'granted') {
|
||
Alert.alert(
|
||
t('medications.detail.photo.permissionDenied', { defaultValue: '权限不足' }),
|
||
t('medications.detail.photo.cameraPermissionMessage', { defaultValue: '需要相机权限以拍摄药品照片' })
|
||
);
|
||
return;
|
||
}
|
||
|
||
const result = await ImagePicker.launchCameraAsync({
|
||
allowsEditing: true,
|
||
quality: 0.3,
|
||
aspect: [9, 16],
|
||
});
|
||
|
||
if (result.canceled || !result.assets?.length) {
|
||
return;
|
||
}
|
||
|
||
const asset = result.assets[0];
|
||
setPhotoPreview(asset.uri);
|
||
|
||
try {
|
||
const { url } = await upload(
|
||
{
|
||
uri: asset.uri,
|
||
name: asset.fileName ?? `medication-${Date.now()}.jpg`,
|
||
type: asset.mimeType ?? 'image/jpeg',
|
||
},
|
||
{ prefix: 'images/medications' }
|
||
);
|
||
|
||
// 上传成功后更新药物信息
|
||
const updated = await dispatch(
|
||
updateMedicationAction({
|
||
id: medication.id,
|
||
photoUrl: url,
|
||
})
|
||
).unwrap();
|
||
setMedication(updated);
|
||
setPhotoPreview(null);
|
||
} catch (uploadError) {
|
||
console.error('[MEDICATION] 图片上传失败', uploadError);
|
||
Alert.alert(
|
||
t('medications.detail.photo.uploadFailed', { defaultValue: '上传失败' }),
|
||
t('medications.detail.photo.uploadFailedMessage', { defaultValue: '图片上传失败,请稍后重试' })
|
||
);
|
||
setPhotoPreview(null);
|
||
}
|
||
} catch (error) {
|
||
console.error('[MEDICATION] 拍照失败', error);
|
||
Alert.alert(
|
||
t('medications.detail.photo.cameraFailed', { defaultValue: '拍照失败' }),
|
||
t('medications.detail.photo.cameraFailedMessage', { defaultValue: '无法打开相机,请稍后再试' })
|
||
);
|
||
}
|
||
},
|
||
},
|
||
{
|
||
text: t('medications.detail.photo.chooseFromLibrary', { defaultValue: '从相册选择' }),
|
||
onPress: async () => {
|
||
try {
|
||
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||
if (permission.status !== 'granted') {
|
||
Alert.alert(
|
||
t('medications.detail.photo.permissionDenied', { defaultValue: '权限不足' }),
|
||
t('medications.detail.photo.libraryPermissionMessage', { defaultValue: '需要相册权限以选择药品照片' })
|
||
);
|
||
return;
|
||
}
|
||
|
||
const result = await ImagePicker.launchImageLibraryAsync({
|
||
mediaTypes: ['images'],
|
||
quality: 0.9,
|
||
});
|
||
|
||
if (result.canceled || !result.assets?.length) {
|
||
return;
|
||
}
|
||
|
||
const asset = result.assets[0];
|
||
setPhotoPreview(asset.uri);
|
||
|
||
try {
|
||
const { url } = await upload(
|
||
{
|
||
uri: asset.uri,
|
||
name: asset.fileName ?? `medication-${Date.now()}.jpg`,
|
||
type: asset.mimeType ?? 'image/jpeg',
|
||
},
|
||
{ prefix: 'images/medications' }
|
||
);
|
||
|
||
// 上传成功后更新药物信息
|
||
const updated = await dispatch(
|
||
updateMedicationAction({
|
||
id: medication.id,
|
||
photoUrl: url,
|
||
})
|
||
).unwrap();
|
||
setMedication(updated);
|
||
setPhotoPreview(null);
|
||
} catch (uploadError) {
|
||
console.error('[MEDICATION] 图片上传失败', uploadError);
|
||
Alert.alert(
|
||
t('medications.detail.photo.uploadFailed', { defaultValue: '上传失败' }),
|
||
t('medications.detail.photo.uploadFailedMessage', { defaultValue: '图片上传失败,请稍后重试' })
|
||
);
|
||
setPhotoPreview(null);
|
||
}
|
||
} catch (error) {
|
||
console.error('[MEDICATION] 从相册选择失败', error);
|
||
Alert.alert(
|
||
t('medications.detail.photo.libraryFailed', { defaultValue: '选择失败' }),
|
||
t('medications.detail.photo.libraryFailedMessage', { defaultValue: '无法打开相册,请稍后再试' })
|
||
);
|
||
}
|
||
},
|
||
},
|
||
{
|
||
text: t('medications.detail.photo.cancel', { defaultValue: '取消' }),
|
||
style: 'cancel',
|
||
},
|
||
],
|
||
{ cancelable: true }
|
||
);
|
||
}, [medication, uploading, upload, dispatch, t]);
|
||
|
||
const handleStartDatePress = useCallback(() => {
|
||
if (!medication) return;
|
||
|
||
const startDate = dayjs(medication.startDate).format('YYYY年M月D日');
|
||
let message;
|
||
if (medication.endDate) {
|
||
const endDate = dayjs(medication.endDate).format('YYYY年M月D日');
|
||
message = t('medications.detail.plan.periodMessage', {
|
||
startDate,
|
||
endDateInfo: t('medications.detail.plan.periodMessage', { endDate })
|
||
});
|
||
} else {
|
||
message = t('medications.detail.plan.periodMessage', {
|
||
startDate,
|
||
endDateInfo: t('medications.detail.plan.longTermPlan')
|
||
});
|
||
}
|
||
|
||
Alert.alert(t('medications.detail.sections.plan'), message);
|
||
}, [medication, t]);
|
||
|
||
const handleTimePress = useCallback(() => {
|
||
Alert.alert(t('medications.detail.plan.time'), t('medications.detail.plan.timeMessage', { times: reminderTimes }));
|
||
}, [reminderTimes, t]);
|
||
|
||
const handleDosagePress = useCallback(() => {
|
||
if (!medication) return;
|
||
setDosagePickerVisible(true);
|
||
}, [medication]);
|
||
|
||
const handleFormPress = useCallback(() => {
|
||
if (!medication) return;
|
||
setFormPickerVisible(true);
|
||
}, [medication]);
|
||
|
||
const handleFrequencyPress = useCallback(() => {
|
||
if (!medication) return;
|
||
// 跳转到独立的频率编辑页面
|
||
router.push({
|
||
pathname: ROUTES.MEDICATION_EDIT_FREQUENCY,
|
||
params: {
|
||
medicationId: medication.id,
|
||
medicationName: medication.name,
|
||
repeatPattern: medication.repeatPattern,
|
||
timesPerDay: medication.timesPerDay.toString(),
|
||
medicationTimes: medication.medicationTimes.join(','),
|
||
},
|
||
});
|
||
}, [medication, router]);
|
||
|
||
const renderAdviceCard = useCallback(
|
||
(
|
||
title: string,
|
||
items: string[] | undefined,
|
||
icon: keyof typeof Ionicons.glyphMap,
|
||
accentColor: string,
|
||
backgroundColor: string
|
||
) => {
|
||
if (!items?.length) return null;
|
||
return (
|
||
<View style={[styles.aiListCard, { backgroundColor, borderColor: backgroundColor }]}>
|
||
<View style={styles.aiListHeader}>
|
||
<View style={[styles.aiListIcon, { backgroundColor: `${accentColor}14` }]}>
|
||
<Ionicons name={icon} size={16} color={accentColor} />
|
||
</View>
|
||
<Text style={[styles.aiListTitle, { color: colors.text }]}>{title}</Text>
|
||
<View style={[styles.aiListCountBadge, { borderColor: `${accentColor}40` }]}>
|
||
<Text style={[styles.aiListCount, { color: accentColor }]}>{items.length}</Text>
|
||
</View>
|
||
</View>
|
||
<View style={styles.aiListContent}>
|
||
{items.map((item, index) => (
|
||
<View key={`${title}-${index}`} style={styles.aiBulletRow}>
|
||
<View style={[styles.aiBulletDot, { backgroundColor: accentColor }]} />
|
||
<Text style={[styles.aiBulletText, { color: colors.text }]}>{item}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
);
|
||
},
|
||
[colors.text]
|
||
);
|
||
|
||
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);
|
||
|
||
// 重新安排药品通知
|
||
try {
|
||
await medicationNotificationService.scheduleMedicationNotifications(updated);
|
||
} catch (error) {
|
||
console.error('[MEDICATION] 安排药品通知失败:', error);
|
||
// 不影响药品更新的成功流程,只记录错误
|
||
}
|
||
} catch (err) {
|
||
console.error('更新剂量失败', err);
|
||
Alert.alert(t('medications.detail.updateErrors.dosage'), t('medications.detail.updateErrors.dosageMessage'));
|
||
} 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);
|
||
|
||
// 重新安排药品通知
|
||
try {
|
||
await medicationNotificationService.scheduleMedicationNotifications(updated);
|
||
} catch (error) {
|
||
console.error('[MEDICATION] 安排药品通知失败:', error);
|
||
// 不影响药品更新的成功流程,只记录错误
|
||
}
|
||
} catch (err) {
|
||
console.error('更新剂型失败', err);
|
||
Alert.alert(t('medications.detail.updateErrors.form'), t('medications.detail.updateErrors.formMessage'));
|
||
} finally {
|
||
setUpdatePending(false);
|
||
}
|
||
}, [dispatch, formPicker, medication, updatePending]);
|
||
|
||
// AI 分析处理函数
|
||
const handleAiAnalysis = useCallback(async () => {
|
||
if (!medication || aiAnalysisLoading) return;
|
||
|
||
// 1. 先验证用户是否登录
|
||
const isLoggedIn = await ensureLoggedIn();
|
||
if (!isLoggedIn) {
|
||
return; // 如果未登录,ensureLoggedIn 会自动跳转到登录页
|
||
}
|
||
|
||
// 2. 检查用户是否是 VIP 或有剩余免费次数
|
||
const serviceAccess = checkServiceAccess();
|
||
if (!serviceAccess.canUseService) {
|
||
setAiAnalysisLocked(true);
|
||
openMembershipModal({
|
||
onPurchaseSuccess: () => {
|
||
handleAiAnalysis();
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 3. 通过验证,执行 AI 分析
|
||
setAiAnalysisLoading(true);
|
||
setAiAnalysisError(null);
|
||
setAiAnalysisLocked(false);
|
||
|
||
// 滚动到底部,让用户看到分析内容
|
||
setTimeout(() => {
|
||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||
}, 100);
|
||
|
||
try {
|
||
const result = await analyzeMedicationV2(medication.id);
|
||
setAiAnalysisResult(result);
|
||
// 本地保存一份,便于下次打开快速展示
|
||
setMedication((prev) => (prev ? { ...prev, aiAnalysis: JSON.stringify(result) } : prev));
|
||
} catch (error: any) {
|
||
console.error('[MEDICATION] AI 分析失败:', error);
|
||
const status = error?.status;
|
||
const message =
|
||
status === 403
|
||
? '免费使用次数已用完,请开通会员获取更多分析次数'
|
||
: status === 404
|
||
? '药品未找到'
|
||
: t('medications.detail.aiAnalysis.error.message');
|
||
|
||
setAiAnalysisError(message);
|
||
if (status === 403) {
|
||
setAiAnalysisLocked(true);
|
||
openMembershipModal({
|
||
onPurchaseSuccess: () => {
|
||
handleAiAnalysis();
|
||
},
|
||
});
|
||
} else {
|
||
Alert.alert(t('medications.detail.aiAnalysis.error.title'), message);
|
||
}
|
||
} finally {
|
||
setAiAnalysisLoading(false);
|
||
}
|
||
}, [aiAnalysisLoading, checkServiceAccess, ensureLoggedIn, medication, openMembershipModal, t]);
|
||
|
||
if (!medicationId) {
|
||
return (
|
||
<View style={styles.container}>
|
||
{/* 背景渐变 */}
|
||
<LinearGradient
|
||
colors={['#f5e5fbff', '#edf4f4ff', '#f7f8f8ff']}
|
||
style={styles.gradientBackground}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 0, y: 1 }}
|
||
/>
|
||
|
||
{/* 装饰性圆圈 */}
|
||
<View style={styles.decorativeCircle1} />
|
||
<View style={styles.decorativeCircle2} />
|
||
|
||
<HeaderBar title={t('medications.detail.title')} variant="minimal" transparent />
|
||
<View style={[styles.centered, { paddingTop: insets.top + 72, paddingHorizontal: 24 }]}>
|
||
<ThemedText style={styles.emptyTitle}>{t('medications.detail.notFound.title')}</ThemedText>
|
||
<ThemedText style={styles.emptySubtitle}>{t('medications.detail.notFound.subtitle')}</ThemedText>
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const isLoadingState = loading && !medication;
|
||
const contentBottomPadding = Math.max(insets.bottom, 16) + 140;
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
{/* 背景渐变 */}
|
||
<LinearGradient
|
||
colors={['#f5e5fbff', '#edf4f4ff', '#f7f8f8ff']}
|
||
style={styles.gradientBackground}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 0, y: 1 }}
|
||
/>
|
||
|
||
{/* 装饰性圆圈 */}
|
||
<View style={styles.decorativeCircle1} />
|
||
<View style={styles.decorativeCircle2} />
|
||
|
||
<HeaderBar title={t('medications.detail.title')} variant="minimal" transparent />
|
||
{isLoadingState ? (
|
||
<View style={[styles.centered, { paddingTop: insets.top + 48 }]}>
|
||
<ActivityIndicator color={colors.primary} />
|
||
<Text style={[styles.loadingText, { color: colors.textSecondary }]}>{t('medications.detail.loading')}</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 }]}>
|
||
{t('medications.detail.error.subtitle')}
|
||
</ThemedText>
|
||
</View>
|
||
) : medication ? (
|
||
<ScrollView
|
||
ref={scrollViewRef}
|
||
contentContainerStyle={[
|
||
styles.content,
|
||
{
|
||
paddingTop: insets.top + 48,
|
||
paddingBottom: contentBottomPadding,
|
||
},
|
||
]}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
<View style={[styles.heroCard, { backgroundColor: colors.surface }]}>
|
||
<View style={styles.heroInfo}>
|
||
<View style={styles.heroImageWrapper}>
|
||
{/* 点击图片区域 - 触发上传 */}
|
||
<TouchableOpacity
|
||
style={styles.heroImageTouchable}
|
||
onPress={handleSelectPhoto}
|
||
activeOpacity={0.8}
|
||
disabled={uploading}
|
||
>
|
||
<Image
|
||
source={
|
||
photoPreview
|
||
? { uri: photoPreview }
|
||
: medication.photoUrl
|
||
? { uri: medication.photoUrl }
|
||
: DEFAULT_IMAGE
|
||
}
|
||
style={styles.heroImage}
|
||
contentFit="cover"
|
||
/>
|
||
</TouchableOpacity>
|
||
|
||
{/* 点击预览图标 - 触发预览(只在有照片且不在上传中时显示) */}
|
||
{medication.photoUrl && !uploading && (
|
||
<TouchableOpacity
|
||
style={styles.imagePreviewHint}
|
||
onPress={handleImagePreview}
|
||
activeOpacity={0.7}
|
||
hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
|
||
>
|
||
<Ionicons name="expand-outline" size={14} color="#FFF" />
|
||
</TouchableOpacity>
|
||
)}
|
||
|
||
{/* 上传中提示 */}
|
||
{uploading && (
|
||
<View style={styles.photoUploadingIndicator}>
|
||
<ActivityIndicator color={colors.primary} size="small" />
|
||
<Text style={[styles.uploadingText, { color: '#FFF' }]}>
|
||
{t('medications.detail.photo.uploading', { defaultValue: '上传中...' })}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
<View style={styles.heroTitleWrapper}>
|
||
<View style={styles.heroTitleRow}>
|
||
<Text style={[styles.heroTitle, { color: colors.text }]} numberOfLines={1}>
|
||
{medication.name}
|
||
</Text>
|
||
<TouchableOpacity
|
||
style={styles.heroEditButton}
|
||
onPress={handleOpenNameModal}
|
||
activeOpacity={0.7}
|
||
hitSlop={8}
|
||
>
|
||
<Ionicons name="create-outline" size={18} color={colors.primary} />
|
||
</TouchableOpacity>
|
||
</View>
|
||
<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={t('medications.detail.sections.plan')} color={colors.text}>
|
||
<View style={styles.row}>
|
||
<InfoCard
|
||
label={t('medications.detail.plan.period')}
|
||
value={medicationPeriodLabel}
|
||
icon="calendar-outline"
|
||
colors={colors}
|
||
clickable={false}
|
||
onPress={handleStartDatePress}
|
||
/>
|
||
<InfoCard
|
||
label={t('medications.detail.plan.time')}
|
||
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 }]}>{t('medications.detail.plan.frequency')}</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={t('medications.detail.sections.dosage')} color={colors.text}>
|
||
<View style={styles.row}>
|
||
<InfoCard
|
||
label={t('medications.detail.dosage.label')}
|
||
value={dosageLabel}
|
||
icon="medkit-outline"
|
||
colors={colors}
|
||
clickable={true}
|
||
onPress={handleDosagePress}
|
||
/>
|
||
<InfoCard
|
||
label={t('medications.detail.dosage.form')}
|
||
value={formLabel}
|
||
icon="cube-outline"
|
||
colors={colors}
|
||
clickable={true}
|
||
onPress={handleFormPress}
|
||
/>
|
||
</View>
|
||
</Section>
|
||
|
||
<Section title={t('medications.detail.sections.note')} 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 }]}>{t('medications.detail.note.label')}</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="AI 分析" color={colors.text}>
|
||
<View style={[styles.aiCardContainer, { backgroundColor: colors.surface }]}>
|
||
<View style={styles.aiHeaderRow}>
|
||
<View style={styles.aiHeaderLeft}>
|
||
<Ionicons name="sparkles-outline" size={18} color={colors.primary} />
|
||
<Text style={[styles.aiHeaderTitle, { color: colors.text }]}>分析结果</Text>
|
||
</View>
|
||
<View
|
||
style={[
|
||
styles.aiStatusPill,
|
||
{ backgroundColor: hasAiAnalysis ? '#E0F2FE' : aiAnalysisLocked ? '#FEF2F2' : '#EEF2FF' },
|
||
]}
|
||
>
|
||
<Ionicons
|
||
name={hasAiAnalysis ? 'checkmark-circle-outline' : aiAnalysisLocked ? 'lock-closed-outline' : 'flash-outline'}
|
||
size={15}
|
||
color={hasAiAnalysis ? '#22c55e' : aiAnalysisLocked ? '#ef4444' : colors.primary}
|
||
/>
|
||
<Text
|
||
style={[
|
||
styles.aiStatusText,
|
||
{ color: hasAiAnalysis ? '#16a34a' : aiAnalysisLocked ? '#ef4444' : colors.primary },
|
||
]}
|
||
>
|
||
{hasAiAnalysis ? '已生成' : aiAnalysisLocked ? '会员专享' : '待生成'}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={styles.aiHeroRow}>
|
||
<View style={styles.aiHeroImageWrapper}>
|
||
<View style={styles.aiHeroImageShadow}>
|
||
<Image
|
||
source={
|
||
photoPreview
|
||
? { uri: photoPreview }
|
||
: medication.photoUrl
|
||
? { uri: medication.photoUrl }
|
||
: DEFAULT_IMAGE
|
||
}
|
||
style={styles.aiHeroImage}
|
||
contentFit="cover"
|
||
/>
|
||
</View>
|
||
{hasAiAnalysis && (
|
||
<LinearGradient
|
||
colors={['#34d399', '#22c55e']}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 1 }}
|
||
style={styles.aiScoreBadge}
|
||
>
|
||
<Ionicons name="thumbs-up-outline" size={14} color="#fff" />
|
||
<Text style={styles.aiScoreBadgeText}>AI 推荐</Text>
|
||
</LinearGradient>
|
||
)}
|
||
</View>
|
||
<View style={styles.aiHeroText}>
|
||
<Text style={[styles.aiHeroTitle, { color: colors.text }]} numberOfLines={2}>
|
||
{medication.name}
|
||
</Text>
|
||
<Text style={[styles.aiHeroSubtitle, { color: colors.textSecondary }]} numberOfLines={3}>
|
||
{aiAnalysisResult?.mainUsage || '获取 AI 分析,快速了解适用人群、成分安全与使用建议。'}
|
||
</Text>
|
||
<View style={styles.aiChipRow}>
|
||
<View style={styles.aiChip}>
|
||
<Ionicons name="medkit-outline" size={14} color={colors.primary} />
|
||
<Text style={[styles.aiChipText, { color: colors.text }]}>{dosageLabel}</Text>
|
||
</View>
|
||
<View style={styles.aiChip}>
|
||
<Ionicons name="cube-outline" size={14} color={colors.primary} />
|
||
<Text style={[styles.aiChipText, { color: colors.text }]}>{formLabel}</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{aiAnalysisLoading && (
|
||
<View style={styles.aiLoadingContainer}>
|
||
<LottieView
|
||
source={require('@/assets/lottie/loading-blue.json')}
|
||
autoPlay
|
||
loop
|
||
style={styles.aiLoadingAnimation}
|
||
/>
|
||
<Text style={[styles.aiLoadingText, { color: colors.textSecondary }]}>
|
||
{t('medications.detail.aiAnalysis.analyzing')}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
{aiAnalysisResult ? (
|
||
<>
|
||
{!!aiAnalysisResult.mainIngredients?.length && (
|
||
<View style={styles.aiTagRow}>
|
||
{aiAnalysisResult.mainIngredients.map((item) => (
|
||
<View key={item} style={styles.aiTag}>
|
||
<Text style={[styles.aiTagText, { color: colors.text }]} numberOfLines={1}>
|
||
{item}
|
||
</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
)}
|
||
|
||
<View style={styles.aiUsageCard}>
|
||
<Ionicons name="leaf-outline" size={18} color="#22c55e" />
|
||
<Text style={[styles.aiUsageText, { color: colors.text }]}>{aiAnalysisResult.mainUsage}</Text>
|
||
</View>
|
||
|
||
<View style={styles.aiColumns}>
|
||
<View style={[styles.aiBubbleCard, { backgroundColor: '#ECFEFF', borderColor: '#BAF2F4' }]}>
|
||
<View style={styles.aiBubbleHeader}>
|
||
<Text style={[styles.aiBubbleTitle, { color: '#0284c7' }]}>适合人群</Text>
|
||
<Ionicons name="checkmark-circle" size={16} color="#0ea5e9" />
|
||
</View>
|
||
{aiAnalysisResult.suitableFor.map((item, idx) => (
|
||
<View key={`s-${idx}`} style={styles.aiBulletRow}>
|
||
<View style={[styles.aiBulletDot, { backgroundColor: '#0ea5e9' }]} />
|
||
<Text style={[styles.aiBulletText, { color: colors.text }]}>{item}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
|
||
<View style={[styles.aiBubbleCard, { backgroundColor: '#FEF2F2', borderColor: '#FEE2E2' }]}>
|
||
<View style={styles.aiBubbleHeader}>
|
||
<Text style={[styles.aiBubbleTitle, { color: '#ef4444' }]}>不适合人群</Text>
|
||
<Ionicons name="alert-circle" size={16} color="#ef4444" />
|
||
</View>
|
||
{aiAnalysisResult.unsuitableFor.map((item, idx) => (
|
||
<View key={`u-${idx}`} style={styles.aiBulletRow}>
|
||
<View style={[styles.aiBulletDot, { backgroundColor: '#ef4444' }]} />
|
||
<Text style={[styles.aiBulletText, { color: colors.text }]}>{item}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
|
||
{renderAdviceCard('可能的副作用', aiAnalysisResult.sideEffects, 'warning-outline', '#f59e0b', '#FFFBEB')}
|
||
{renderAdviceCard('储存建议', aiAnalysisResult.storageAdvice, 'cube-outline', '#10b981', '#ECFDF3')}
|
||
{renderAdviceCard('健康/使用建议', aiAnalysisResult.healthAdvice, 'sparkles-outline', '#6366f1', '#EEF2FF')}
|
||
</>
|
||
) : null}
|
||
|
||
{aiAnalysisError && (
|
||
<View style={styles.aiErrorBox}>
|
||
<Ionicons name="alert-circle-outline" size={18} color="#ef4444" />
|
||
<Text style={[styles.aiErrorText, { color: colors.text }]}>{aiAnalysisError}</Text>
|
||
</View>
|
||
)}
|
||
|
||
{(aiAnalysisLocked || !serviceInfo.canUseService) && (
|
||
<TouchableOpacity
|
||
activeOpacity={0.9}
|
||
onPress={() =>
|
||
openMembershipModal({
|
||
onPurchaseSuccess: () => {
|
||
handleAiAnalysis();
|
||
},
|
||
})
|
||
}
|
||
style={styles.aiMembershipCard}
|
||
>
|
||
<View style={styles.aiMembershipLeft}>
|
||
<Ionicons name="diamond-outline" size={18} color="#f59e0b" />
|
||
<View>
|
||
<Text style={styles.aiMembershipTitle}>会员专享 AI 深度解读</Text>
|
||
<Text style={styles.aiMembershipSub}>解锁完整药品分析与无限次使用</Text>
|
||
</View>
|
||
</View>
|
||
<Ionicons name="chevron-forward" size={18} color="#f59e0b" />
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
</Section>
|
||
|
||
<Section title={t('medications.detail.sections.overview')} 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 ? t('medications.detail.overview.calculating') : t('medications.detail.overview.takenCount', { count: summary.takenCount })}
|
||
</Text>
|
||
<Text style={[styles.summaryMeta, { color: colors.textSecondary }]}>
|
||
{summaryLoading ? t('medications.detail.overview.calculatingDays') : dayStreakText}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</Section>
|
||
|
||
|
||
</ScrollView>
|
||
) : null}
|
||
|
||
{medication ? (
|
||
<View
|
||
style={[
|
||
styles.footerBar,
|
||
{
|
||
paddingBottom: Math.max(insets.bottom, 18),
|
||
},
|
||
]}
|
||
>
|
||
<View style={styles.footerButtonContainer}>
|
||
{/* AI 分析按钮 */}
|
||
{!hasAiAnalysis && (
|
||
<TouchableOpacity
|
||
style={styles.aiAnalysisButtonWrapper}
|
||
activeOpacity={0.9}
|
||
onPress={handleAiAnalysis}
|
||
disabled={aiAnalysisLoading}
|
||
>
|
||
{isLiquidGlassAvailable() ? (
|
||
<GlassView
|
||
style={styles.aiAnalysisButton}
|
||
glassEffectStyle="regular"
|
||
tintColor="rgba(59, 130, 246, 0.8)"
|
||
isInteractive={true}
|
||
>
|
||
{aiAnalysisLoading ? (
|
||
<ActivityIndicator size="small" color="#fff" />
|
||
) : (
|
||
<Ionicons name="sparkles-outline" size={18} color="#fff" />
|
||
)}
|
||
<Text style={styles.aiAnalysisButtonText}>
|
||
{aiActionLabel}
|
||
</Text>
|
||
</GlassView>
|
||
) : (
|
||
<View style={[styles.aiAnalysisButton, styles.fallbackAiButton]}>
|
||
{aiAnalysisLoading ? (
|
||
<ActivityIndicator size="small" color="#fff" />
|
||
) : (
|
||
<Ionicons name="sparkles-outline" size={18} color="#fff" />
|
||
)}
|
||
<Text style={styles.aiAnalysisButtonText}>
|
||
{aiActionLabel}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</TouchableOpacity>
|
||
)}
|
||
|
||
{/* 删除按钮 */}
|
||
<TouchableOpacity
|
||
style={styles.deleteButtonWrapper}
|
||
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={24} color="#fff" />
|
||
</GlassView>
|
||
) : (
|
||
<View style={[styles.deleteButton, styles.fallbackDeleteButton]}>
|
||
<Ionicons name="trash-outline" size={24} color="#fff" />
|
||
</View>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
) : null}
|
||
|
||
<Modal
|
||
transparent
|
||
animationType="fade"
|
||
visible={nameModalVisible}
|
||
onRequestClose={handleCloseNameModal}
|
||
>
|
||
<View style={styles.modalOverlay}>
|
||
<TouchableOpacity style={styles.modalBackdrop} activeOpacity={1} onPress={handleCloseNameModal} />
|
||
<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 }]}>
|
||
{t('medications.detail.nameEdit.title', { defaultValue: '编辑药物名称' })}
|
||
</Text>
|
||
<TouchableOpacity onPress={handleCloseNameModal} hitSlop={12}>
|
||
<Ionicons name="close" size={20} color={colors.textSecondary} />
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<View
|
||
style={[
|
||
styles.nameInputWrapper,
|
||
{
|
||
backgroundColor: scheme === 'light' ? '#F2F2F7' : 'rgba(255, 255, 255, 0.08)',
|
||
},
|
||
]}
|
||
>
|
||
<TextInput
|
||
value={nameDraft}
|
||
onChangeText={handleNameChange}
|
||
placeholder={t('medications.detail.nameEdit.placeholder', { defaultValue: '请输入药物名称' })}
|
||
placeholderTextColor={colors.textMuted}
|
||
style={[styles.nameInput, { color: colors.text }]}
|
||
autoFocus
|
||
returnKeyType="done"
|
||
onSubmitEditing={handleSaveName}
|
||
selectionColor={colors.primary}
|
||
clearButtonMode="while-editing"
|
||
/>
|
||
</View>
|
||
<View style={styles.nameInputCounterWrapper}>
|
||
<Text style={[styles.nameInputCounter, { color: colors.textMuted }]}>
|
||
{`${Array.from(nameDraft).length}/10`}
|
||
</Text>
|
||
</View>
|
||
|
||
<View style={styles.modalActionContainer}>
|
||
<TouchableOpacity
|
||
style={[
|
||
styles.modalActionPrimary,
|
||
{
|
||
backgroundColor: colors.primary,
|
||
shadowColor: colors.primary,
|
||
},
|
||
]}
|
||
onPress={handleSaveName}
|
||
activeOpacity={0.9}
|
||
disabled={nameSaving}
|
||
>
|
||
{nameSaving ? (
|
||
<ActivityIndicator color={colors.onPrimary} size="small" />
|
||
) : (
|
||
<Text style={[styles.modalActionPrimaryText, { color: colors.onPrimary }]}>
|
||
{t('medications.detail.nameEdit.saveButton', { defaultValue: '保存' })}
|
||
</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
|
||
<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 }]}>{t('medications.detail.note.edit')}</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={t('medications.detail.note.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 }]}>
|
||
{t('medications.detail.note.voiceNotSupported')}
|
||
</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 }]}>{t('medications.detail.note.save')}</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 }]}>
|
||
{t('medications.detail.dosage.selectDosage')}
|
||
</ThemedText>
|
||
<View style={styles.pickerRow}>
|
||
<View style={styles.pickerColumn}>
|
||
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
|
||
{t('medications.detail.dosage.dosageValue')}
|
||
</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 }]}>
|
||
{t('medications.detail.dosage.unit')}
|
||
</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 }]}>
|
||
{t('medications.detail.pickers.cancel')}
|
||
</ThemedText>
|
||
</Pressable>
|
||
<Pressable
|
||
onPress={confirmDosagePicker}
|
||
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
|
||
>
|
||
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
|
||
{t('medications.detail.pickers.confirm')}
|
||
</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 }]}>
|
||
{t('medications.detail.dosage.selectForm')}
|
||
</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 }]}>
|
||
{t('medications.detail.pickers.cancel')}
|
||
</ThemedText>
|
||
</Pressable>
|
||
<Pressable
|
||
onPress={confirmFormPicker}
|
||
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
|
||
>
|
||
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
|
||
{t('medications.detail.pickers.confirm')}
|
||
</ThemedText>
|
||
</Pressable>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
|
||
{medication ? (
|
||
<ConfirmationSheet
|
||
visible={deleteSheetVisible}
|
||
onClose={() => setDeleteSheetVisible(false)}
|
||
onConfirm={handleDeleteMedication}
|
||
title={t('medications.detail.delete.title', { name: medication.name })}
|
||
description={t('medications.detail.delete.description')}
|
||
confirmText={t('medications.detail.delete.confirm')}
|
||
cancelText={t('medications.detail.delete.cancel')}
|
||
destructive
|
||
loading={deleteLoading}
|
||
/>
|
||
) : null}
|
||
|
||
{medication ? (
|
||
<ConfirmationSheet
|
||
visible={deactivateSheetVisible}
|
||
onClose={() => setDeactivateSheetVisible(false)}
|
||
onConfirm={handleDeactivateMedication}
|
||
title={t('medications.detail.deactivate.title', { name: medication.name })}
|
||
description={t('medications.detail.deactivate.description')}
|
||
confirmText={t('medications.detail.deactivate.confirm')}
|
||
cancelText={t('medications.detail.deactivate.cancel')}
|
||
destructive
|
||
loading={deactivateLoading}
|
||
/>
|
||
) : 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}>{t('medications.detail.imageViewer.close')}</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',
|
||
},
|
||
gradientBackground: {
|
||
position: 'absolute',
|
||
left: 0,
|
||
right: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
},
|
||
decorativeCircle1: {
|
||
position: 'absolute',
|
||
top: 40,
|
||
right: 20,
|
||
width: 60,
|
||
height: 60,
|
||
borderRadius: 30,
|
||
backgroundColor: '#0EA5E9',
|
||
opacity: 0.1,
|
||
},
|
||
decorativeCircle2: {
|
||
position: 'absolute',
|
||
bottom: -15,
|
||
left: -15,
|
||
width: 40,
|
||
height: 40,
|
||
borderRadius: 20,
|
||
backgroundColor: '#0EA5E9',
|
||
opacity: 0.05,
|
||
},
|
||
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',
|
||
backgroundColor: '#FFFFFF',
|
||
alignItems: 'center',
|
||
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',
|
||
overflow: 'hidden',
|
||
},
|
||
heroImageTouchable: {
|
||
width: '100%',
|
||
height: '100%',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
heroImage: {
|
||
width: '60%',
|
||
height: '60%',
|
||
borderRadius: '20%'
|
||
},
|
||
heroTitleWrapper: {
|
||
flex: 1,
|
||
minWidth: 0,
|
||
},
|
||
heroTitleRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
},
|
||
heroTitle: {
|
||
fontSize: 20,
|
||
fontWeight: '700',
|
||
},
|
||
heroEditButton: {
|
||
padding: 4,
|
||
borderRadius: 12,
|
||
},
|
||
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,
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 12,
|
||
shadowOffset: { width: 0, height: 3 },
|
||
elevation: 3,
|
||
borderWidth: 1,
|
||
borderColor: 'rgba(0, 0, 0, 0.04)',
|
||
},
|
||
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',
|
||
},
|
||
nameInputWrapper: {
|
||
borderRadius: 18,
|
||
paddingHorizontal: 18,
|
||
paddingVertical: 14,
|
||
},
|
||
nameInput: {
|
||
fontSize: 18,
|
||
fontWeight: '600',
|
||
},
|
||
nameInputCounterWrapper: {
|
||
alignItems: 'flex-end',
|
||
},
|
||
nameInputCounter: {
|
||
fontSize: 12,
|
||
fontWeight: '500',
|
||
},
|
||
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: {
|
||
width: 56,
|
||
height: 56,
|
||
borderRadius: 28,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
overflow: 'hidden', // 保证玻璃边界圆角效果
|
||
},
|
||
fallbackDeleteButton: {
|
||
backgroundColor: '#EF4444',
|
||
shadowColor: 'rgba(239,68,68,0.4)',
|
||
shadowOffset: { width: 0, height: 10 },
|
||
shadowOpacity: 1,
|
||
shadowRadius: 20,
|
||
elevation: 6,
|
||
},
|
||
// 底部按钮容器样式
|
||
footerButtonContainer: {
|
||
flexDirection: 'row',
|
||
gap: 12,
|
||
justifyContent: 'flex-end',
|
||
},
|
||
aiAnalysisButtonWrapper: {
|
||
flex: 1,
|
||
},
|
||
deleteButtonWrapper: {
|
||
// auto width
|
||
},
|
||
aiAnalysisButton: {
|
||
height: 56,
|
||
borderRadius: 24,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 8,
|
||
overflow: 'hidden',
|
||
},
|
||
fallbackAiButton: {
|
||
backgroundColor: '#3B82F6',
|
||
shadowColor: 'rgba(59,130,246,0.4)',
|
||
shadowOffset: { width: 0, height: 10 },
|
||
shadowOpacity: 1,
|
||
shadowRadius: 20,
|
||
elevation: 6,
|
||
},
|
||
aiAnalysisButtonText: {
|
||
fontSize: 17,
|
||
fontWeight: '700',
|
||
color: '#fff',
|
||
},
|
||
// AI 分析卡片样式
|
||
aiCardContainer: {
|
||
borderRadius: 26,
|
||
padding: 20,
|
||
gap: 14,
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 12,
|
||
shadowOffset: { width: 0, height: 3 },
|
||
elevation: 3,
|
||
borderWidth: 1,
|
||
borderColor: 'rgba(0, 0, 0, 0.04)',
|
||
},
|
||
aiHeaderRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
},
|
||
aiHeaderLeft: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
},
|
||
aiHeaderTitle: {
|
||
fontSize: 17,
|
||
fontWeight: '700',
|
||
},
|
||
aiStatusPill: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 6,
|
||
borderRadius: 14,
|
||
},
|
||
aiStatusText: {
|
||
fontSize: 12,
|
||
fontWeight: '700',
|
||
},
|
||
aiLoadingContainer: {
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingVertical: 24,
|
||
gap: 12,
|
||
},
|
||
aiLoadingAnimation: {
|
||
width: 120,
|
||
height: 120,
|
||
},
|
||
aiLoadingText: {
|
||
fontSize: 14,
|
||
fontWeight: '500',
|
||
},
|
||
aiHeroRow: {
|
||
flexDirection: 'row',
|
||
gap: 14,
|
||
alignItems: 'center',
|
||
},
|
||
aiHeroImageWrapper: {
|
||
width: 110,
|
||
height: 110,
|
||
borderRadius: 28,
|
||
backgroundColor: '#F8FAFC',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
position: 'relative',
|
||
},
|
||
aiHeroImageShadow: {
|
||
width: 90,
|
||
height: 90,
|
||
borderRadius: 22,
|
||
overflow: 'hidden',
|
||
backgroundColor: '#fff',
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.08,
|
||
shadowRadius: 8,
|
||
shadowOffset: { width: 0, height: 4 },
|
||
elevation: 3,
|
||
},
|
||
aiHeroImage: {
|
||
width: '100%',
|
||
height: '100%',
|
||
borderRadius: 22,
|
||
},
|
||
aiScoreBadge: {
|
||
position: 'absolute',
|
||
bottom: -6,
|
||
right: -4,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 6,
|
||
borderRadius: 14,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
shadowColor: '#22c55e',
|
||
shadowOpacity: 0.25,
|
||
shadowRadius: 8,
|
||
shadowOffset: { width: 0, height: 4 },
|
||
elevation: 4,
|
||
},
|
||
aiScoreBadgeText: {
|
||
fontSize: 12,
|
||
fontWeight: '700',
|
||
color: '#fff',
|
||
},
|
||
aiHeroText: {
|
||
flex: 1,
|
||
gap: 8,
|
||
},
|
||
aiHeroTitle: {
|
||
fontSize: 20,
|
||
fontWeight: '800',
|
||
},
|
||
aiHeroSubtitle: {
|
||
fontSize: 14,
|
||
lineHeight: 20,
|
||
},
|
||
aiChipRow: {
|
||
flexDirection: 'row',
|
||
gap: 8,
|
||
flexWrap: 'wrap',
|
||
},
|
||
aiChip: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 8,
|
||
backgroundColor: '#F3F4F6',
|
||
borderRadius: 12,
|
||
},
|
||
aiChipText: {
|
||
fontSize: 13,
|
||
fontWeight: '600',
|
||
},
|
||
aiTagRow: {
|
||
flexDirection: 'row',
|
||
flexWrap: 'wrap',
|
||
gap: 8,
|
||
},
|
||
aiTag: {
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 8,
|
||
borderRadius: 14,
|
||
backgroundColor: '#F2FCE2',
|
||
},
|
||
aiTagText: {
|
||
fontSize: 13,
|
||
fontWeight: '600',
|
||
},
|
||
aiUsageCard: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 10,
|
||
padding: 14,
|
||
borderRadius: 18,
|
||
backgroundColor: '#F8FAFC',
|
||
borderWidth: 1,
|
||
borderColor: 'rgba(15,23,42,0.05)',
|
||
},
|
||
aiUsageText: {
|
||
fontSize: 15,
|
||
lineHeight: 22,
|
||
flex: 1,
|
||
},
|
||
aiColumns: {
|
||
flexDirection: 'row',
|
||
gap: 12,
|
||
flexWrap: 'wrap',
|
||
},
|
||
aiBubbleCard: {
|
||
flex: 1,
|
||
borderWidth: 1,
|
||
borderRadius: 18,
|
||
padding: 14,
|
||
gap: 8,
|
||
},
|
||
aiBubbleHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
marginBottom: 4,
|
||
},
|
||
aiBubbleTitle: {
|
||
fontSize: 15,
|
||
fontWeight: '700',
|
||
},
|
||
aiListCard: {
|
||
borderWidth: 1,
|
||
borderRadius: 18,
|
||
padding: 14,
|
||
gap: 12,
|
||
},
|
||
aiListHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 10,
|
||
},
|
||
aiListIcon: {
|
||
width: 28,
|
||
height: 28,
|
||
borderRadius: 14,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
aiListTitle: {
|
||
fontSize: 15,
|
||
fontWeight: '700',
|
||
flex: 1,
|
||
},
|
||
aiListCountBadge: {
|
||
borderRadius: 10,
|
||
borderWidth: 1,
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 2,
|
||
},
|
||
aiListCount: {
|
||
fontSize: 12,
|
||
fontWeight: '700',
|
||
},
|
||
aiListContent: {
|
||
gap: 8,
|
||
},
|
||
aiBulletRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'flex-start',
|
||
gap: 8,
|
||
},
|
||
aiBulletDot: {
|
||
width: 8,
|
||
height: 8,
|
||
borderRadius: 4,
|
||
marginTop: 6,
|
||
},
|
||
aiBulletText: {
|
||
fontSize: 14,
|
||
lineHeight: 22,
|
||
flex: 1,
|
||
},
|
||
aiEmptyBox: {
|
||
borderRadius: 18,
|
||
padding: 16,
|
||
backgroundColor: '#F8FAFF',
|
||
borderWidth: 1,
|
||
borderColor: '#E0EAFF',
|
||
gap: 8,
|
||
alignItems: 'center',
|
||
},
|
||
aiEmptyTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
},
|
||
aiEmptyDesc: {
|
||
fontSize: 14,
|
||
lineHeight: 22,
|
||
textAlign: 'center',
|
||
},
|
||
aiFreeBadge: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 8,
|
||
backgroundColor: '#EEF2FF',
|
||
borderRadius: 12,
|
||
},
|
||
aiFreeBadgeText: {
|
||
fontSize: 12,
|
||
fontWeight: '700',
|
||
},
|
||
aiErrorBox: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 10,
|
||
padding: 12,
|
||
borderRadius: 14,
|
||
backgroundColor: '#FEF2F2',
|
||
borderWidth: 1,
|
||
borderColor: '#FEE2E2',
|
||
},
|
||
aiErrorText: {
|
||
fontSize: 14,
|
||
lineHeight: 20,
|
||
flex: 1,
|
||
},
|
||
aiMembershipCard: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
padding: 14,
|
||
borderRadius: 18,
|
||
backgroundColor: '#FFFBEB',
|
||
borderWidth: 1,
|
||
borderColor: '#FDE68A',
|
||
},
|
||
aiMembershipLeft: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 10,
|
||
flex: 1,
|
||
},
|
||
aiMembershipTitle: {
|
||
fontSize: 15,
|
||
fontWeight: '700',
|
||
color: '#92400e',
|
||
},
|
||
aiMembershipSub: {
|
||
fontSize: 13,
|
||
color: '#b45309',
|
||
marginTop: 2,
|
||
},
|
||
// 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',
|
||
},
|
||
imagePreviewHint: {
|
||
position: 'absolute',
|
||
top: 4,
|
||
right: 4,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||
borderRadius: 10,
|
||
padding: 4,
|
||
zIndex: 10,
|
||
},
|
||
photoUploadingIndicator: {
|
||
position: 'absolute',
|
||
bottom: 8,
|
||
left: 8,
|
||
right: 8,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 6,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||
borderRadius: 12,
|
||
},
|
||
uploadingText: {
|
||
fontSize: 11,
|
||
fontWeight: '600',
|
||
color: '#FFF',
|
||
},
|
||
// 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',
|
||
},
|
||
});
|