feat(medications): 添加药品图片预览功能并优化InfoCard组件
- 在药品详情页面集成 react-native-image-viewing 实现图片全屏预览 - 添加图片预览提示图标,提升用户交互体验 - 优化 InfoCard 组件渲染逻辑,简化代码结构 - 调整药品图片样式,增加圆角效果并优化尺寸比例 - 为可点击的 InfoCard 图标和箭头添加玻璃态效果支持
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import ImageViewing from 'react-native-image-viewing';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png');
|
const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png');
|
||||||
@@ -79,6 +80,7 @@ export default function MedicationDetailScreen() {
|
|||||||
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||||
const [deleteSheetVisible, setDeleteSheetVisible] = useState(false);
|
const [deleteSheetVisible, setDeleteSheetVisible] = useState(false);
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||||
|
const [showImagePreview, setShowImagePreview] = useState(false);
|
||||||
|
|
||||||
// 剂量选择相关状态
|
// 剂量选择相关状态
|
||||||
const [dosagePickerVisible, setDosagePickerVisible] = useState(false);
|
const [dosagePickerVisible, setDosagePickerVisible] = useState(false);
|
||||||
@@ -518,6 +520,12 @@ export default function MedicationDetailScreen() {
|
|||||||
}
|
}
|
||||||
}, [deleteLoading, dispatch, medication, router]);
|
}, [deleteLoading, dispatch, medication, router]);
|
||||||
|
|
||||||
|
const handleImagePreview = useCallback(() => {
|
||||||
|
if (medication?.photoUrl) {
|
||||||
|
setShowImagePreview(true);
|
||||||
|
}
|
||||||
|
}, [medication?.photoUrl]);
|
||||||
|
|
||||||
const handleStartDatePress = useCallback(() => {
|
const handleStartDatePress = useCallback(() => {
|
||||||
Alert.alert('开始日期', `开始服药日期:${startDateLabel}`);
|
Alert.alert('开始日期', `开始服药日期:${startDateLabel}`);
|
||||||
}, [startDateLabel]);
|
}, [startDateLabel]);
|
||||||
@@ -743,13 +751,23 @@ export default function MedicationDetailScreen() {
|
|||||||
>
|
>
|
||||||
<View style={[styles.heroCard, { backgroundColor: colors.surface }]}>
|
<View style={[styles.heroCard, { backgroundColor: colors.surface }]}>
|
||||||
<View style={styles.heroInfo}>
|
<View style={styles.heroInfo}>
|
||||||
<View style={styles.heroImageWrapper}>
|
<TouchableOpacity
|
||||||
|
style={styles.heroImageWrapper}
|
||||||
|
onPress={handleImagePreview}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
disabled={!medication.photoUrl}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
source={medication.photoUrl ? { uri: medication.photoUrl } : DEFAULT_IMAGE}
|
source={medication.photoUrl ? { uri: medication.photoUrl } : DEFAULT_IMAGE}
|
||||||
style={styles.heroImage}
|
style={styles.heroImage}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
/>
|
/>
|
||||||
|
{medication.photoUrl && (
|
||||||
|
<View style={styles.imagePreviewHint}>
|
||||||
|
<Ionicons name="expand-outline" size={14} color="#FFF" />
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
<View>
|
<View>
|
||||||
<Text style={[styles.heroTitle, { color: colors.text }]}>{medication.name}</Text>
|
<Text style={[styles.heroTitle, { color: colors.text }]}>{medication.name}</Text>
|
||||||
<Text style={[styles.heroMeta, { color: colors.textSecondary }]}>
|
<Text style={[styles.heroMeta, { color: colors.textSecondary }]}>
|
||||||
@@ -1305,6 +1323,35 @@ export default function MedicationDetailScreen() {
|
|||||||
loading={deleteLoading}
|
loading={deleteLoading}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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}>关闭</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1381,10 +1428,12 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#F2F2F2',
|
backgroundColor: '#F2F2F2',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
},
|
},
|
||||||
heroImage: {
|
heroImage: {
|
||||||
width: '80%',
|
width: '60%',
|
||||||
height: '80%',
|
height: '60%',
|
||||||
|
borderRadius: '20%'
|
||||||
},
|
},
|
||||||
heroTitle: {
|
heroTitle: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
@@ -1709,4 +1758,49 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: '600',
|
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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,10 +24,72 @@ export const InfoCard: React.FC<InfoCardProps> = ({
|
|||||||
onPress,
|
onPress,
|
||||||
clickable = false,
|
clickable = false,
|
||||||
glassEffectStyle = 'clear',
|
glassEffectStyle = 'clear',
|
||||||
|
tintColor,
|
||||||
}) => {
|
}) => {
|
||||||
const isGlassAvailable = isLiquidGlassAvailable();
|
const isGlassAvailable = isLiquidGlassAvailable();
|
||||||
|
|
||||||
|
// 渲染图标按钮 - 只在可点击时应用 GlassView
|
||||||
|
const renderIcon = () => {
|
||||||
|
if (clickable && isGlassAvailable) {
|
||||||
|
return (
|
||||||
|
<GlassView
|
||||||
|
style={styles.infoCardIcon}
|
||||||
|
glassEffectStyle={glassEffectStyle}
|
||||||
|
tintColor={tintColor || 'rgba(76, 110, 245, 0.2)'}
|
||||||
|
isInteractive={true}
|
||||||
|
>
|
||||||
|
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
||||||
|
</GlassView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[
|
||||||
|
styles.infoCardIcon,
|
||||||
|
clickable && styles.clickableIconFallback
|
||||||
|
]}>
|
||||||
|
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染箭头 - 只在可点击时显示并应用 GlassView
|
||||||
|
const renderArrow = () => {
|
||||||
|
if (!clickable) return null;
|
||||||
|
|
||||||
|
if (isGlassAvailable) {
|
||||||
|
return (
|
||||||
|
<GlassView
|
||||||
|
style={styles.infoCardArrow}
|
||||||
|
glassEffectStyle={glassEffectStyle}
|
||||||
|
tintColor={tintColor || 'rgba(255, 255, 255, 0.3)'}
|
||||||
|
isInteractive={true}
|
||||||
|
>
|
||||||
|
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||||
|
</GlassView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.infoCardArrow, styles.arrowFallback]}>
|
||||||
|
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 卡片内容
|
||||||
|
const cardContent = (
|
||||||
|
<View style={[
|
||||||
|
styles.infoCard,
|
||||||
|
{ backgroundColor: colors.surface || '#fff' }
|
||||||
|
]}>
|
||||||
|
{renderArrow()}
|
||||||
|
{renderIcon()}
|
||||||
|
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
||||||
|
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
// 如果可点击且有onPress回调,使用TouchableOpacity包装
|
// 如果可点击且有onPress回调,使用TouchableOpacity包装
|
||||||
if (clickable && onPress) {
|
if (clickable && onPress) {
|
||||||
return (
|
return (
|
||||||
@@ -36,35 +98,7 @@ export const InfoCard: React.FC<InfoCardProps> = ({
|
|||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
{isGlassAvailable ? (
|
{cardContent}
|
||||||
<GlassView
|
|
||||||
style={[
|
|
||||||
styles.infoCard,
|
|
||||||
]}
|
|
||||||
glassEffectStyle={glassEffectStyle}
|
|
||||||
isInteractive={true}
|
|
||||||
>
|
|
||||||
<View style={styles.infoCardArrow}>
|
|
||||||
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
|
||||||
</View>
|
|
||||||
<View style={styles.infoCardIcon}>
|
|
||||||
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
|
||||||
</View>
|
|
||||||
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
|
||||||
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
|
||||||
</GlassView>
|
|
||||||
) : (
|
|
||||||
<View style={[styles.infoCard]}>
|
|
||||||
<View style={styles.infoCardArrow}>
|
|
||||||
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
|
||||||
</View>
|
|
||||||
<View style={styles.infoCardIcon}>
|
|
||||||
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
|
||||||
</View>
|
|
||||||
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
|
||||||
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,32 +106,7 @@ export const InfoCard: React.FC<InfoCardProps> = ({
|
|||||||
// 不可点击的版本
|
// 不可点击的版本
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{isGlassAvailable ? (
|
{cardContent}
|
||||||
<GlassView
|
|
||||||
style={[
|
|
||||||
styles.infoCard,
|
|
||||||
{
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
borderColor: `${colors.border}80`,
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
glassEffectStyle={glassEffectStyle}
|
|
||||||
>
|
|
||||||
<View style={styles.infoCardIcon}>
|
|
||||||
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
|
||||||
</View>
|
|
||||||
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
|
||||||
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
|
||||||
</GlassView>
|
|
||||||
) : (
|
|
||||||
<View style={[styles.infoCard, { backgroundColor: colors.surface }]}>
|
|
||||||
<View style={styles.infoCardIcon}>
|
|
||||||
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
|
||||||
</View>
|
|
||||||
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
|
||||||
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -118,13 +127,23 @@ const styles = StyleSheet.create({
|
|||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden', // 保证玻璃边界圆角效果
|
|
||||||
},
|
},
|
||||||
infoCardArrow: {
|
infoCardArrow: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 12,
|
top: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden', // 保证玻璃边界圆角效果
|
||||||
|
},
|
||||||
|
arrowFallback: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(0, 0, 0, 0.1)',
|
||||||
},
|
},
|
||||||
infoCardIcon: {
|
infoCardIcon: {
|
||||||
width: 28,
|
width: 28,
|
||||||
@@ -133,6 +152,11 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#EEF1FF',
|
backgroundColor: '#EEF1FF',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden', // 保证玻璃边界圆角效果
|
||||||
|
},
|
||||||
|
clickableIconFallback: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(76, 110, 245, 0.3)',
|
||||||
},
|
},
|
||||||
infoCardLabel: {
|
infoCardLabel: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|||||||
Reference in New Issue
Block a user