feat(medications): 重构药品通知系统并添加独立设置页面

- 创建药品通知服务模块,统一管理药品提醒通知的调度和取消
- 新增独立的通知设置页面,支持总开关和药品提醒开关分离控制
- 重构药品详情页面,移除频率编辑功能到独立页面
- 优化药品添加流程,支持拍照和相册选择图片
- 改进通知权限检查和错误处理机制
- 更新用户偏好设置,添加药品提醒开关配置
This commit is contained in:
richarjiang
2025-11-11 16:43:27 +08:00
parent d9975813cb
commit f4ce3d9edf
12 changed files with 1551 additions and 524 deletions

View File

@@ -7,6 +7,7 @@ import { useAppDispatch } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useCosUpload } from '@/hooks/useCosUpload';
import { medicationNotificationService } from '@/services/medicationNotifications';
import { createMedicationAction, fetchMedicationRecords, fetchMedications } from '@/store/medicationsSlice';
import type { MedicationForm, RepeatPattern } from '@/types/medication';
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
@@ -288,6 +289,16 @@ export default function AddMedicationScreen() {
const today = dayjs().format('YYYY-MM-DD');
await dispatch(fetchMedicationRecords({ date: today }));
// 重新安排药品通知
try {
// 获取最新的药品列表
const medications = await dispatch(fetchMedications({ isActive: true })).unwrap();
await medicationNotificationService.rescheduleAllMedicationNotifications(medications);
} catch (error) {
console.error('[MEDICATION] 安排药品通知失败:', error);
// 不影响添加药品的成功流程,只记录错误
}
// 成功提示
Alert.alert(
'添加成功',
@@ -357,42 +368,99 @@ export default function AddMedicationScreen() {
}
}, [dictationActive, dictationLoading, isDictationSupported]);
const handleTakePhoto = useCallback(async () => {
try {
const permission = await ImagePicker.requestCameraPermissionsAsync();
if (permission.status !== 'granted') {
Alert.alert('权限不足', '需要相机权限以拍摄药品照片');
return;
}
// 处理图片选择(拍照或相册)
const handleSelectPhoto = useCallback(() => {
Alert.alert(
'选择图片',
'请选择图片来源',
[
{
text: '拍照',
onPress: async () => {
try {
const permission = await ImagePicker.requestCameraPermissionsAsync();
if (permission.status !== 'granted') {
Alert.alert('权限不足', '需要相机权限以拍摄药品照片');
return;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.9,
});
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
quality: 0.9,
aspect: [9,16]
});
if (result.canceled || !result.assets?.length) {
return;
}
if (result.canceled || !result.assets?.length) {
return;
}
const asset = result.assets[0];
setPhotoPreview(asset.uri);
setPhotoUrl(null);
const asset = result.assets[0];
setPhotoPreview(asset.uri);
setPhotoUrl(null);
try {
const { url } = await upload(
{ uri: asset.uri, name: asset.fileName ?? `medication-${Date.now()}.jpg`, type: asset.mimeType ?? 'image/jpeg' },
{ prefix: 'images/medications' }
);
setPhotoUrl(url);
} catch (uploadError) {
console.error('[MEDICATION] 图片上传失败', uploadError);
Alert.alert('上传失败', '图片上传失败,请稍后重试');
}
} catch (error) {
console.error('[MEDICATION] 拍照失败', error);
Alert.alert('拍照失败', '无法打开相机,请稍后再试');
}
try {
const { url } = await upload(
{ uri: asset.uri, name: asset.fileName ?? `medication-${Date.now()}.jpg`, type: asset.mimeType ?? 'image/jpeg' },
{ prefix: 'images/medications' }
);
setPhotoUrl(url);
} catch (uploadError) {
console.error('[MEDICATION] 图片上传失败', uploadError);
Alert.alert('上传失败', '图片上传失败,请稍后重试');
}
} catch (error) {
console.error('[MEDICATION] 拍照失败', error);
Alert.alert('拍照失败', '无法打开相机,请稍后再试');
}
},
},
{
text: '从相册选择',
onPress: async () => {
try {
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permission.status !== 'granted') {
Alert.alert('权限不足', '需要相册权限以选择药品照片');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.9,
});
if (result.canceled || !result.assets?.length) {
return;
}
const asset = result.assets[0];
setPhotoPreview(asset.uri);
setPhotoUrl(null);
try {
const { url } = await upload(
{ uri: asset.uri, name: asset.fileName ?? `medication-${Date.now()}.jpg`, type: asset.mimeType ?? 'image/jpeg' },
{ prefix: 'images/medications' }
);
setPhotoUrl(url);
} catch (uploadError) {
console.error('[MEDICATION] 图片上传失败', uploadError);
Alert.alert('上传失败', '图片上传失败,请稍后重试');
}
} catch (error) {
console.error('[MEDICATION] 从相册选择失败', error);
Alert.alert('选择失败', '无法打开相册,请稍后再试');
}
},
},
{
text: '取消',
style: 'cancel',
},
],
{ cancelable: true }
);
}, [upload]);
const handleRemovePhoto = useCallback(() => {
@@ -539,7 +607,7 @@ export default function AddMedicationScreen() {
backgroundColor: colors.surface,
},
]}
onPress={handleTakePhoto}
onPress={handleSelectPhoto}
disabled={uploading}
>
{photoPreview ? (
@@ -548,7 +616,7 @@ export default function AddMedicationScreen() {
<View style={styles.photoOverlay}>
<Ionicons name="camera" size={18} color="#fff" />
<ThemedText style={styles.photoOverlayText}>
{uploading ? '上传中…' : '重新拍摄'}
{uploading ? '上传中…' : '重新选择'}
</ThemedText>
</View>
<Pressable style={styles.photoRemoveBtn} onPress={handleRemovePhoto} hitSlop={12}>
@@ -560,8 +628,8 @@ export default function AddMedicationScreen() {
<View style={[styles.photoIconBadge, { backgroundColor: `${colors.primary}12` }]}>
<Ionicons name="camera" size={22} color={colors.primary} />
</View>
<ThemedText style={[styles.photoTitle, { color: colors.text }]}></ThemedText>
<ThemedText style={[styles.photoSubtitle, { color: colors.textMuted }]}></ThemedText>
<ThemedText style={[styles.photoTitle, { color: colors.text }]}></ThemedText>
<ThemedText style={[styles.photoSubtitle, { color: colors.textMuted }]}></ThemedText>
</View>
)}
{uploading && (