From ee60f0756e21dec6e59c32d794c79cdb8f2e0d9a Mon Sep 17 00:00:00 2001 From: richarjiang Date: Wed, 19 Nov 2025 16:08:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(medication):=20=E6=B7=BB=E5=8A=A0=E8=8D=AF?= =?UTF-8?q?=E7=89=A9=E5=90=8D=E7=A7=B0=E7=BC=96=E8=BE=91=E5=92=8C=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/medications/[medicationId].tsx | 407 +++++++++++++++++++++++++++-- 1 file changed, 391 insertions(+), 16 deletions(-) diff --git a/app/medications/[medicationId].tsx b/app/medications/[medicationId].tsx index 11ff9e7..f8ee39c 100644 --- a/app/medications/[medicationId].tsx +++ b/app/medications/[medicationId].tsx @@ -9,6 +9,7 @@ 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'; @@ -30,6 +31,7 @@ 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 React, { useCallback, useEffect, useMemo, useState } from 'react'; @@ -73,6 +75,7 @@ export default function MedicationDetailScreen() { 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); @@ -89,6 +92,9 @@ export default function MedicationDetailScreen() { 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'; @@ -98,6 +104,7 @@ export default function MedicationDetailScreen() { const [deactivateSheetVisible, setDeactivateSheetVisible] = useState(false); const [deactivateLoading, setDeactivateLoading] = useState(false); const [showImagePreview, setShowImagePreview] = useState(false); + const [photoPreview, setPhotoPreview] = useState(null); // AI 分析相关状态 const [aiAnalysisLoading, setAiAnalysisLoading] = useState(false); @@ -151,6 +158,9 @@ export default function MedicationDetailScreen() { useEffect(() => { setNoteDraft(medication?.note ?? ''); }, [medication?.note]); + useEffect(() => { + setNameDraft(medication?.name ?? ''); + }, [medication?.name]); useEffect(() => { let isMounted = true; @@ -480,6 +490,59 @@ export default function MedicationDetailScreen() { 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(); @@ -548,6 +611,146 @@ export default function MedicationDetailScreen() { } }, [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; @@ -849,25 +1052,63 @@ export default function MedicationDetailScreen() { > - - - {medication.photoUrl && ( - + + {/* 点击图片区域 - 触发上传 */} + + + + + {/* 点击预览图标 - 触发预览(只在有照片且不在上传中时显示) */} + {medication.photoUrl && !uploading && ( + + + )} + + {/* 上传中提示 */} + {uploading && ( + + + + {t('medications.detail.photo.uploading', { defaultValue: '上传中...' })} + )} - - - {medication.name} + + + + + {medication.name} + + + + + {dosageLabel} · {formLabel} @@ -1151,6 +1392,85 @@ export default function MedicationDetailScreen() { ) : null} + + + + + + + + + {t('medications.detail.nameEdit.title', { defaultValue: '编辑药物名称' })} + + + + + + + + + + + + {`${Array.from(nameDraft).length}/10`} + + + + + + {nameSaving ? ( + + ) : ( + + {t('medications.detail.nameEdit.saveButton', { defaultValue: '保存' })} + + )} + + + + + + +