Files
digital-pilates/components/medications/MedicationPhotoGuideModal.tsx
richarjiang bcb910140e feat(medications): 添加AI智能识别药品功能和有效期管理
- 新增AI药品识别流程,支持多角度拍摄和实时进度显示
- 添加药品有效期字段,支持在添加和编辑药品时设置有效期
- 新增MedicationAddOptionsSheet选择录入方式(AI识别/手动录入)
- 新增ai-camera和ai-progress两个独立页面处理AI识别流程
- 新增ExpiryDatePickerModal和MedicationPhotoGuideModal组件
- 移除本地通知系统,迁移到服务端推送通知
- 添加medicationNotificationCleanup服务清理旧的本地通知
- 更新药品详情页支持AI草稿模式和有效期显示
- 优化药品表单,支持有效期选择和AI识别结果确认
- 更新i18n资源,添加有效期相关翻译

BREAKING CHANGE: 药品通知系统从本地通知迁移到服务端推送,旧版本的本地通知将被清理
2025-11-21 17:32:44 +08:00

265 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Ionicons } from '@expo/vector-icons';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import React from 'react';
import {
Dimensions,
Modal,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
interface MedicationPhotoGuideModalProps {
visible: boolean;
onClose: () => void;
}
/**
* 药品拍摄指南弹窗组件
* 展示如何正确拍摄药品照片的说明和示例
*/
export function MedicationPhotoGuideModal({ visible, onClose }: MedicationPhotoGuideModalProps) {
return (
<Modal
visible={visible}
transparent={true}
animationType="fade"
onRequestClose={onClose}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={onClose}
>
<TouchableOpacity
activeOpacity={1}
onPress={(e) => e.stopPropagation()}
style={styles.guideModalContainer}
>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.guideModalContent}
>
{/* 标题部分 */}
<View style={styles.guideHeader}>
<Text style={styles.guideStepBadge}></Text>
<Text style={styles.guideTitle}></Text>
</View>
{/* 示例图片 */}
<View style={styles.guideImagesContainer}>
{/* 正确示例 */}
<View style={styles.guideImageWrapper}>
<View style={styles.guideImageBox}>
<Ionicons
name="checkmark-circle"
size={32}
color="#4CAF50"
style={styles.guideImageIcon}
/>
<Image
source={require('@/assets/images/medicine/image-medicine.png')}
style={styles.guideImage}
contentFit="cover"
/>
</View>
<View style={styles.guideImageIndicator}>
<Ionicons name="checkmark-circle" size={20} color="#4CAF50" />
</View>
</View>
{/* 错误示例 */}
<View style={styles.guideImageWrapper}>
<View style={[styles.guideImageBox, styles.guideImageBoxBlur]}>
<Ionicons
name="close-circle"
size={32}
color="#F44336"
style={styles.guideImageIcon}
/>
<Image
source={require('@/assets/images/medicine/image-medicine.png')}
style={[styles.guideImage, { opacity: 0.5 }]}
contentFit="cover"
blurRadius={8}
/>
</View>
<View style={[styles.guideImageIndicator, styles.guideImageIndicatorError]}>
<Ionicons name="close-circle" size={20} color="#F44336" />
</View>
</View>
</View>
{/* 说明文字 */}
<View style={styles.guideDescription}>
<Text style={styles.guideDescriptionText}>
\\
</Text>
<Text style={styles.guideDescriptionText}>
线
</Text>
</View>
{/* 确认按钮 */}
<TouchableOpacity
onPress={onClose}
activeOpacity={0.8}
>
{isLiquidGlassAvailable() ? (
<GlassView
style={styles.guideConfirmButton}
glassEffectStyle="regular"
tintColor="rgba(255, 179, 0, 0.9)"
isInteractive={true}
>
<LinearGradient
colors={['rgba(255, 179, 0, 0.95)', 'rgba(255, 160, 0, 0.95)']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.guideConfirmButtonGradient}
>
<Text style={styles.guideConfirmButtonText}></Text>
</LinearGradient>
</GlassView>
) : (
<View style={styles.guideConfirmButton}>
<LinearGradient
colors={['#FFB300', '#FFA000']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.guideConfirmButtonGradient}
>
<Text style={styles.guideConfirmButtonText}></Text>
</LinearGradient>
</View>
)}
</TouchableOpacity>
</ScrollView>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
);
}
const styles = StyleSheet.create({
modalOverlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
guideModalContainer: {
width: SCREEN_WIDTH - 48,
maxHeight: '80%',
backgroundColor: '#FFFFFF',
borderRadius: 24,
overflow: 'hidden',
shadowColor: '#000',
shadowOpacity: 0.25,
shadowRadius: 20,
shadowOffset: { width: 0, height: 10 },
elevation: 10,
},
guideModalContent: {
padding: 24,
},
guideHeader: {
alignItems: 'center',
marginBottom: 24,
},
guideStepBadge: {
fontSize: 16,
fontWeight: '700',
color: '#FFB300',
marginBottom: 8,
},
guideTitle: {
fontSize: 22,
fontWeight: '700',
color: '#0f172a',
textAlign: 'center',
},
guideImagesContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 24,
gap: 12,
},
guideImageWrapper: {
flex: 1,
alignItems: 'center',
},
guideImageBox: {
width: '100%',
aspectRatio: 1,
borderRadius: 16,
overflow: 'hidden',
backgroundColor: '#f8fafc',
position: 'relative',
borderWidth: 2,
borderColor: '#4CAF50',
},
guideImageBoxBlur: {
borderColor: '#F44336',
},
guideImage: {
width: '100%',
height: '100%',
},
guideImageIcon: {
position: 'absolute',
top: 8,
left: 8,
zIndex: 1,
},
guideImageIndicator: {
marginTop: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
},
guideImageIndicatorError: {
backgroundColor: 'rgba(244, 67, 54, 0.1)',
},
guideDescription: {
backgroundColor: '#f8fafc',
borderRadius: 16,
padding: 16,
marginBottom: 24,
},
guideDescriptionText: {
fontSize: 14,
lineHeight: 22,
color: '#475569',
marginBottom: 8,
},
guideConfirmButton: {
borderRadius: 16,
overflow: 'hidden',
shadowColor: '#FFB300',
shadowOpacity: 0.3,
shadowRadius: 12,
shadowOffset: { width: 0, height: 6 },
elevation: 6,
},
guideConfirmButtonGradient: {
paddingVertical: 16,
alignItems: 'center',
justifyContent: 'center',
},
guideConfirmButtonText: {
fontSize: 18,
fontWeight: '700',
color: '#FFFFFF',
},
});