Files
digital-pilates/app/medications/[medicationId].tsx
richarjiang 2dca3253e6 feat(i18n): 实现应用国际化支持,添加中英文翻译
- 为所有UI组件添加国际化支持,替换硬编码文本
- 新增useI18n钩子函数统一管理翻译
- 完善中英文翻译资源,覆盖统计、用药、通知设置等模块
- 优化Tab布局使用翻译键值替代静态文本
- 更新药品管理、个人资料编辑等页面的多语言支持
2025-11-13 11:09:55 +08:00

1939 lines
62 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_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 { useI18n } from '@/hooks/useI18n';
import { useVipService } from '@/hooks/useVipService';
import { medicationNotificationService } from '@/services/medicationNotifications';
import {
analyzeMedicationStream,
getMedicationById,
getMedicationRecords,
} from '@/services/medications';
import {
deleteMedicationAction,
fetchMedications,
selectMedications,
updateMedicationAction,
} from '@/store/medicationsSlice';
import type { Medication, 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 { LinearGradient } from 'expo-linear-gradient';
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 Markdown from 'react-native-markdown-display';
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 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 [deactivateSheetVisible, setDeactivateSheetVisible] = useState(false);
const [deactivateLoading, setDeactivateLoading] = useState(false);
const [showImagePreview, setShowImagePreview] = useState(false);
// AI 分析相关状态
const [aiAnalysisLoading, setAiAnalysisLoading] = useState(false);
const [aiAnalysisContent, setAiAnalysisContent] = useState('');
const [aiAnalysisAbortController, setAiAnalysisAbortController] = useState<AbortController | null>(null);
// 剂量选择相关状态
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(() => {
if (medicationFromStore) {
setMedication(medicationFromStore);
setLoading(false);
// 如果服务端返回了 AI 分析结果,自动展示
if (medicationFromStore.aiAnalysis) {
setAiAnalysisContent(medicationFromStore.aiAnalysis);
}
}
}, [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(() => {
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 分析结果,自动展示
if (data.aiAnalysis) {
setAiAnalysisContent(data.aiAnalysis);
}
})
.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) {
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(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 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(t('medications.detail.note.saveError.title'), t('medications.detail.note.saveError.message'));
} finally {
setNoteSaving(false);
}
}, [closeNoteModal, dispatch, medication, noteDraft]);
const statusLabel = medication?.isActive ? t('medications.detail.status.enabled') : t('medications.detail.status.disabled');
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 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 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) {
// 如果不能使用服务,弹出会员购买弹窗
openMembershipModal({
onPurchaseSuccess: () => {
// 购买成功后自动执行 AI 分析
handleAiAnalysis();
},
});
return;
}
// 3. 通过验证,执行 AI 分析
// 重置状态
setAiAnalysisContent('');
setAiAnalysisLoading(true);
// 滚动到底部,让用户看到分析内容
setTimeout(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, 100);
// 创建 AbortController 用于取消
const controller = new AbortController();
setAiAnalysisAbortController(controller);
try {
await analyzeMedicationStream(
medication.id,
{
onChunk: (chunk: string) => {
setAiAnalysisContent((prev) => prev + chunk);
},
onEnd: () => {
setAiAnalysisLoading(false);
setAiAnalysisAbortController(null);
// 重新加载药品详情以获取最新的 aiAnalysis 字段
if (medicationId) {
getMedicationById(medicationId)
.then((data) => {
setMedication(data);
})
.catch((err) => {
console.error('重新加载药品详情失败', err);
});
}
},
onError: (error: any) => {
console.error('[MEDICATION] AI 分析失败:', error);
let errorMessage = t('medications.detail.aiAnalysis.error.message');
// 解析服务端返回的错误信息
if (error?.message) {
if (error.message.includes('[ERROR]')) {
errorMessage = error.message.replace('[ERROR]', '').trim();
} else if (error.message.includes('无权访问')) {
errorMessage = t('medications.detail.aiAnalysis.error.forbidden');
} else if (error.message.includes('不存在')) {
errorMessage = t('medications.detail.aiAnalysis.error.notFound');
}
} else if (error?.status === 401) {
errorMessage = t('medications.detail.aiAnalysis.error.unauthorized');
} else if (error?.status === 403) {
errorMessage = t('medications.detail.aiAnalysis.error.forbidden');
} else if (error?.status === 404) {
errorMessage = t('medications.detail.aiAnalysis.error.notFound');
}
// 使用 Alert 弹窗显示错误
Alert.alert(t('medications.detail.aiAnalysis.error.title'), errorMessage);
// 清空内容和加载状态
setAiAnalysisContent('');
setAiAnalysisLoading(false);
setAiAnalysisAbortController(null);
},
}
);
} catch (error) {
console.error('[MEDICATION] AI 分析异常:', error);
// 使用 Alert 弹窗显示错误
Alert.alert(t('medications.detail.aiAnalysis.error.title'), t('medications.detail.aiAnalysis.error.networkError'));
// 清空内容和加载状态
setAiAnalysisContent('');
setAiAnalysisLoading(false);
setAiAnalysisAbortController(null);
}
}, [medication, aiAnalysisLoading, ensureLoggedIn, checkServiceAccess, openMembershipModal]);
// 组件卸载时取消 AI 分析请求
useEffect(() => {
return () => {
if (aiAnalysisAbortController) {
aiAnalysisAbortController.abort();
}
};
}, [aiAnalysisAbortController]);
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}>
<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={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={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>
{/* AI 分析结果展示 - 移动到底部 */}
{(aiAnalysisContent || aiAnalysisLoading) && (
<Section title={t('medications.detail.sections.aiAnalysis')} color={colors.text}>
<View style={[styles.aiAnalysisCard, { backgroundColor: colors.surface }]}>
{aiAnalysisLoading && !aiAnalysisContent && (
<View style={styles.aiAnalysisLoading}>
<ActivityIndicator color={colors.primary} size="small" />
<Text style={[styles.aiAnalysisLoadingText, { color: colors.textSecondary }]}>
{t('medications.detail.aiAnalysis.analyzing')}
</Text>
</View>
)}
{aiAnalysisContent && (
<View style={styles.aiAnalysisContentWrapper}>
<Markdown
style={{
body: { color: colors.text, fontSize: 15, lineHeight: 24 },
paragraph: { marginTop: 2, marginBottom: 8 },
bullet_list: { marginVertical: 8 },
ordered_list: { marginVertical: 8 },
list_item: { flexDirection: 'row', marginVertical: 2 },
code_inline: {
backgroundColor: 'rgba(0,0,0,0.06)',
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
},
code_block: {
backgroundColor: 'rgba(0,0,0,0.06)',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 8,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
},
fence: {
backgroundColor: 'rgba(0,0,0,0.06)',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 8,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
},
heading1: { fontSize: 20, fontWeight: '800', marginVertical: 8, color: colors.text },
heading2: { fontSize: 18, fontWeight: '800', marginVertical: 8, color: colors.text },
heading3: { fontSize: 16, fontWeight: '700', marginVertical: 6, color: colors.text },
heading4: { fontSize: 15, fontWeight: '700', marginVertical: 4, color: colors.text },
strong: { fontWeight: '700', color: colors.text },
em: { fontStyle: 'italic' },
link: { color: colors.primary, textDecorationLine: 'underline' },
blockquote: {
backgroundColor: 'rgba(0,0,0,0.03)',
borderLeftWidth: 4,
borderLeftColor: colors.primary,
paddingHorizontal: 12,
paddingVertical: 8,
marginVertical: 8,
},
hr: {
backgroundColor: colors.border,
height: 1,
marginVertical: 12,
},
table: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 8,
marginVertical: 8,
},
thead: {
backgroundColor: 'rgba(0,0,0,0.03)',
},
tr: {
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
th: {
padding: 8,
fontWeight: '700',
},
td: {
padding: 8,
},
}}
>
{aiAnalysisContent}
</Markdown>
{aiAnalysisLoading && (
<View style={styles.aiAnalysisStreaming}>
<ActivityIndicator color={colors.primary} size="small" />
</View>
)}
</View>
)}
</View>
</Section>
)}
</ScrollView>
) : null}
{medication ? (
<View
style={[
styles.footerBar,
{
paddingBottom: Math.max(insets.bottom, 18),
},
]}
>
<View style={styles.footerButtonContainer}>
{/* AI 分析按钮 - 占 2/3 宽度 */}
<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}>
{aiAnalysisLoading ? t('medications.detail.aiAnalysis.analyzingButton') : t('medications.detail.aiAnalysis.button')}
</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}>
{aiAnalysisLoading ? t('medications.detail.aiAnalysis.analyzingButton') : t('medications.detail.aiAnalysis.button')}
</Text>
</View>
)}
</TouchableOpacity>
{/* 删除按钮 - 占 1/3 宽度 */}
<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={18} color="#fff" />
</GlassView>
) : (
<View style={[styles.deleteButton, styles.fallbackDeleteButton]}>
<Ionicons name="trash-outline" size={18} color="#fff" />
</View>
)}
</TouchableOpacity>
</View>
</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 }]}>{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',
},
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,
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',
},
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,
},
// 底部按钮容器样式
footerButtonContainer: {
flexDirection: 'row',
gap: 12,
},
aiAnalysisButtonWrapper: {
flex: 2, // 占 2/3 宽度
},
deleteButtonWrapper: {
flex: 1, // 占 1/3 宽度
},
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 分析卡片样式
aiAnalysisCard: {
borderRadius: 24,
padding: 20,
minHeight: 100,
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 12,
shadowOffset: { width: 0, height: 3 },
elevation: 3,
borderWidth: 1,
borderColor: 'rgba(0, 0, 0, 0.04)',
},
aiAnalysisLoading: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
paddingVertical: 12,
},
aiAnalysisLoadingText: {
fontSize: 15,
},
aiAnalysisContentWrapper: {
gap: 12,
},
aiAnalysisText: {
fontSize: 15,
lineHeight: 24,
},
aiAnalysisStreaming: {
alignSelf: 'flex-start',
marginTop: 8,
},
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',
},
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',
},
});