feat(medications): 添加药品图片预览功能并优化InfoCard组件

- 在药品详情页面集成 react-native-image-viewing 实现图片全屏预览
- 添加图片预览提示图标,提升用户交互体验
- 优化 InfoCard 组件渲染逻辑,简化代码结构
- 调整药品图片样式,增加圆角效果并优化尺寸比例
- 为可点击的 InfoCard 图标和箭头添加玻璃态效果支持
This commit is contained in:
richarjiang
2025-11-11 14:40:26 +08:00
parent 7ea558847d
commit d9975813cb
2 changed files with 179 additions and 61 deletions

View File

@@ -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',
},
}); });

View File

@@ -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,