- 新增AI药品识别流程,支持多角度拍摄和实时进度显示 - 添加药品有效期字段,支持在添加和编辑药品时设置有效期 - 新增MedicationAddOptionsSheet选择录入方式(AI识别/手动录入) - 新增ai-camera和ai-progress两个独立页面处理AI识别流程 - 新增ExpiryDatePickerModal和MedicationPhotoGuideModal组件 - 移除本地通知系统,迁移到服务端推送通知 - 添加medicationNotificationCleanup服务清理旧的本地通知 - 更新药品详情页支持AI草稿模式和有效期显示 - 优化药品表单,支持有效期选择和AI识别结果确认 - 更新i18n资源,添加有效期相关翻译 BREAKING CHANGE: 药品通知系统从本地通知迁移到服务端推送,旧版本的本地通知将被清理
265 lines
7.5 KiB
TypeScript
265 lines
7.5 KiB
TypeScript
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',
|
||
},
|
||
}); |