feat(medications): 添加药品详情页面和删除功能

新增药品详情页面,支持查看药品信息、编辑备注、切换提醒状态和删除药品
- 创建动态路由页面 /medications/[medicationId].tsx 展示药品详细信息
- 添加语音输入备注功能,支持 iOS 语音识别
- 实现药品删除确认对话框和删除操作
- 优化药品卡片点击跳转详情页面的交互
- 添加删除操作的加载状态和错误处理
- 改进药品管理页面的开关状态显示和加载指示器
This commit is contained in:
richarjiang
2025-11-10 14:46:13 +08:00
parent 25b8e45af8
commit 0594831c9f
6 changed files with 1375 additions and 46 deletions

View File

@@ -0,0 +1,265 @@
import * as Haptics from 'expo-haptics';
import React, { useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
Animated,
Dimensions,
Modal,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const { height: screenHeight } = Dimensions.get('window');
interface ConfirmationSheetProps {
visible: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
description?: string;
confirmText?: string;
cancelText?: string;
destructive?: boolean;
loading?: boolean;
}
export function ConfirmationSheet({
visible,
onClose,
onConfirm,
title,
description,
confirmText = '确认',
cancelText = '取消',
destructive = false,
loading = false,
}: ConfirmationSheetProps) {
const insets = useSafeAreaInsets();
const translateY = useRef(new Animated.Value(screenHeight)).current;
const backdropOpacity = useRef(new Animated.Value(0)).current;
const [modalVisible, setModalVisible] = useState(visible);
useEffect(() => {
if (visible) {
setModalVisible(true);
}
}, [visible]);
useEffect(() => {
if (!modalVisible) {
return;
}
if (visible) {
translateY.setValue(screenHeight);
backdropOpacity.setValue(0);
Animated.parallel([
Animated.timing(backdropOpacity, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}),
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
bounciness: 6,
speed: 12,
}),
]).start();
return;
}
Animated.parallel([
Animated.timing(backdropOpacity, {
toValue: 0,
duration: 150,
useNativeDriver: true,
}),
Animated.timing(translateY, {
toValue: screenHeight,
duration: 240,
useNativeDriver: true,
}),
]).start(() => {
translateY.setValue(screenHeight);
backdropOpacity.setValue(0);
setModalVisible(false);
});
}, [visible, modalVisible, backdropOpacity, translateY]);
const handleCancel = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onClose();
};
const handleConfirm = () => {
if (loading) return;
Haptics.notificationAsync(
destructive ? Haptics.NotificationFeedbackType.Error : Haptics.NotificationFeedbackType.Success
);
onConfirm();
};
if (!modalVisible) {
return null;
}
return (
<Modal
visible={modalVisible}
transparent
animationType="none"
onRequestClose={onClose}
statusBarTranslucent
>
<View style={styles.overlay}>
<Animated.View
style={[
styles.backdrop,
{
opacity: backdropOpacity,
},
]}
>
<TouchableOpacity style={StyleSheet.absoluteFillObject} activeOpacity={1} onPress={handleCancel} />
</Animated.View>
<Animated.View
style={[
styles.sheet,
{
transform: [{ translateY }],
paddingBottom: Math.max(insets.bottom, 20),
},
]}
>
<View style={styles.handle} />
<Text style={styles.title}>{title}</Text>
{description ? <Text style={styles.description}>{description}</Text> : null}
<View style={styles.actions}>
<TouchableOpacity
style={styles.cancelButton}
activeOpacity={0.85}
onPress={handleCancel}
disabled={loading}
>
<Text style={styles.cancelText}>{cancelText}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.confirmButton,
destructive ? styles.destructiveButton : styles.primaryButton,
loading && styles.disabledButton,
]}
activeOpacity={0.85}
onPress={handleConfirm}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.confirmText}>{confirmText}</Text>
)}
</TouchableOpacity>
</View>
</Animated.View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'transparent',
},
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(15, 23, 42, 0.45)',
},
sheet: {
backgroundColor: '#fff',
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
paddingHorizontal: 24,
paddingTop: 16,
shadowColor: '#000',
shadowOpacity: 0.12,
shadowRadius: 16,
shadowOffset: { width: 0, height: -4 },
elevation: 16,
gap: 12,
},
handle: {
width: 50,
height: 4,
borderRadius: 2,
backgroundColor: '#E5E7EB',
alignSelf: 'center',
marginBottom: 8,
},
title: {
fontSize: 18,
fontWeight: '700',
color: '#111827',
textAlign: 'center',
},
description: {
fontSize: 15,
color: '#6B7280',
textAlign: 'center',
lineHeight: 22,
},
actions: {
flexDirection: 'row',
gap: 12,
marginTop: 8,
},
cancelButton: {
flex: 1,
height: 56,
borderRadius: 18,
borderWidth: 1,
borderColor: '#E5E7EB',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F8FAFC',
},
cancelText: {
fontSize: 16,
fontWeight: '600',
color: '#111827',
},
confirmButton: {
flex: 1,
height: 56,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
shadowColor: 'rgba(239, 68, 68, 0.45)',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 1,
shadowRadius: 20,
elevation: 6,
},
primaryButton: {
backgroundColor: '#2563EB',
},
destructiveButton: {
backgroundColor: '#EF4444',
},
disabledButton: {
opacity: 0.7,
},
confirmText: {
fontSize: 16,
fontWeight: '700',
color: '#fff',
},
});