feat(medications): 添加AI智能识别药品功能和有效期管理
- 新增AI药品识别流程,支持多角度拍摄和实时进度显示 - 添加药品有效期字段,支持在添加和编辑药品时设置有效期 - 新增MedicationAddOptionsSheet选择录入方式(AI识别/手动录入) - 新增ai-camera和ai-progress两个独立页面处理AI识别流程 - 新增ExpiryDatePickerModal和MedicationPhotoGuideModal组件 - 移除本地通知系统,迁移到服务端推送通知 - 添加medicationNotificationCleanup服务清理旧的本地通知 - 更新药品详情页支持AI草稿模式和有效期显示 - 优化药品表单,支持有效期选择和AI识别结果确认 - 更新i18n资源,添加有效期相关翻译 BREAKING CHANGE: 药品通知系统从本地通知迁移到服务端推送,旧版本的本地通知将被清理
This commit is contained in:
409
components/medication/MedicationAddOptionsSheet.tsx
Normal file
409
components/medication/MedicationAddOptionsSheet.tsx
Normal file
@@ -0,0 +1,409 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Animated, Modal, Pressable, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onManualAdd: () => void;
|
||||
onAiRecognize: () => void;
|
||||
};
|
||||
|
||||
export function MedicationAddOptionsSheet({ visible, onClose, onManualAdd, onAiRecognize }: Props) {
|
||||
const translateY = useRef(new Animated.Value(300)).current;
|
||||
const opacity = useRef(new Animated.Value(0)).current;
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
// 打开时:先显示 Modal,然后执行动画
|
||||
setModalVisible(true);
|
||||
Animated.parallel([
|
||||
Animated.spring(translateY, {
|
||||
toValue: 0,
|
||||
tension: 65,
|
||||
friction: 11,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(opacity, {
|
||||
toValue: 1,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
} else if (modalVisible) {
|
||||
// 关闭时:先执行动画,动画完成后隐藏 Modal
|
||||
Animated.parallel([
|
||||
Animated.timing(translateY, {
|
||||
toValue: 300,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(opacity, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(({ finished }) => {
|
||||
if (finished) {
|
||||
setModalVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [visible, modalVisible, opacity, translateY]);
|
||||
|
||||
const handleClose = () => {
|
||||
// 触发关闭动画
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal visible={modalVisible} transparent animationType="none" onRequestClose={handleClose}>
|
||||
<Pressable style={styles.overlay} onPress={onClose}>
|
||||
<Animated.View style={[styles.backdrop, { opacity }]} />
|
||||
</Pressable>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{
|
||||
transform: [{ translateY }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<Text style={styles.title}>添加药物</Text>
|
||||
<Text style={styles.subtitle}>选择录入方式</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={handleClose} style={styles.closeButton} activeOpacity={0.7}>
|
||||
<Ionicons name="close" size={24} color="#64748b" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* AI 智能识别 - 主推荐 */}
|
||||
<TouchableOpacity activeOpacity={0.95} onPress={onAiRecognize}>
|
||||
<LinearGradient
|
||||
colors={['#0ea5e9', '#0284c7']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.aiCard}
|
||||
>
|
||||
{/* 推荐标签 */}
|
||||
<View style={styles.recommendBadge}>
|
||||
<Ionicons name="sparkles" size={14} color="#fbbf24" />
|
||||
<Text style={styles.recommendText}>推荐使用</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.aiContent}>
|
||||
<View style={styles.aiLeft}>
|
||||
<View style={styles.aiIconWrapper}>
|
||||
<Ionicons name="camera" size={32} color="#fff" />
|
||||
</View>
|
||||
<View style={styles.aiTexts}>
|
||||
<Text style={styles.aiTitle}>AI 智能识别</Text>
|
||||
<Text style={styles.aiDescription}>
|
||||
拍照识别药品信息{'\n'}自动生成提醒计划
|
||||
</Text>
|
||||
<View style={styles.aiFeatures}>
|
||||
<View style={styles.featureItem}>
|
||||
<Ionicons name="flash" size={14} color="#fff" />
|
||||
<Text style={styles.featureText}>快速识别</Text>
|
||||
</View>
|
||||
<View style={styles.featureItem}>
|
||||
<Ionicons name="checkmark-circle" size={14} color="#fff" />
|
||||
<Text style={styles.featureText}>智能填充</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Image
|
||||
source={require('@/assets/images/medicine/image-medicine.png')}
|
||||
style={styles.aiImage}
|
||||
contentFit="contain"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* AI 说明 */}
|
||||
<View style={styles.aiFooter}>
|
||||
<Ionicons name="information-circle-outline" size={14} color="rgba(255,255,255,0.8)" />
|
||||
<Text style={styles.aiFooterText}>需会员或 AI 次数 · 拍摄时确保光线充足</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 分隔线 */}
|
||||
<View style={styles.divider}>
|
||||
<View style={styles.dividerLine} />
|
||||
<Text style={styles.dividerText}>或</Text>
|
||||
<View style={styles.dividerLine} />
|
||||
</View>
|
||||
|
||||
{/* 手动录入 - 次要选项 */}
|
||||
<TouchableOpacity activeOpacity={0.9} onPress={onManualAdd}>
|
||||
<View style={styles.manualCard}>
|
||||
<View style={styles.manualLeft}>
|
||||
<View style={styles.manualIconWrapper}>
|
||||
<Ionicons name="create-outline" size={24} color="#6366f1" />
|
||||
</View>
|
||||
<View style={styles.manualTexts}>
|
||||
<Text style={styles.manualTitle}>手动录入</Text>
|
||||
<Text style={styles.manualDescription}>
|
||||
逐项填写药品信息和服用计划
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.manualRight}>
|
||||
<View style={styles.manualBadge}>
|
||||
<Text style={styles.manualBadgeText}>免费</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={20} color="#94a3b8" />
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 底部安全距离 */}
|
||||
<View style={styles.safeArea} />
|
||||
</Animated.View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
},
|
||||
sheet: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: '#fff',
|
||||
borderTopLeftRadius: 32,
|
||||
borderTopRightRadius: 32,
|
||||
paddingTop: 24,
|
||||
paddingHorizontal: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 20,
|
||||
shadowOffset: { width: 0, height: -8 },
|
||||
elevation: 12,
|
||||
},
|
||||
|
||||
// Header
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 24,
|
||||
},
|
||||
headerLeft: {
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
color: '#0f172a',
|
||||
marginBottom: 4,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
fontWeight: '500',
|
||||
},
|
||||
closeButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: '#f1f5f9',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 12,
|
||||
},
|
||||
|
||||
// AI 卡片 - 主推荐
|
||||
aiCard: {
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
marginBottom: 20,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#0ea5e9',
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 16,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
elevation: 8,
|
||||
},
|
||||
recommendBadge: {
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
backgroundColor: 'rgba(255,255,255,0.25)',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.3)',
|
||||
},
|
||||
recommendText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
aiContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
aiLeft: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
gap: 16,
|
||||
},
|
||||
aiIconWrapper: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(255,255,255,0.3)',
|
||||
},
|
||||
aiTexts: {
|
||||
flex: 1,
|
||||
gap: 8,
|
||||
},
|
||||
aiTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
aiDescription: {
|
||||
fontSize: 14,
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
lineHeight: 20,
|
||||
},
|
||||
aiFeatures: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
featureItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
featureText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
aiImage: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
marginLeft: 12,
|
||||
},
|
||||
aiFooter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: 'rgba(255,255,255,0.2)',
|
||||
},
|
||||
aiFooterText: {
|
||||
fontSize: 12,
|
||||
color: 'rgba(255,255,255,0.8)',
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
// 分隔线
|
||||
divider: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
dividerLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: '#e2e8f0',
|
||||
},
|
||||
dividerText: {
|
||||
fontSize: 13,
|
||||
color: '#94a3b8',
|
||||
fontWeight: '600',
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
|
||||
// 手动录入卡片 - 次要选项
|
||||
manualCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: '#f8fafc',
|
||||
borderRadius: 20,
|
||||
padding: 16,
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
manualLeft: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
manualIconWrapper: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#eef2ff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
manualTexts: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
manualTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#0f172a',
|
||||
},
|
||||
manualDescription: {
|
||||
fontSize: 13,
|
||||
color: '#64748b',
|
||||
lineHeight: 18,
|
||||
},
|
||||
manualRight: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginLeft: 12,
|
||||
},
|
||||
manualBadge: {
|
||||
backgroundColor: '#dcfce7',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
manualBadgeText: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
color: '#16a34a',
|
||||
},
|
||||
|
||||
// 底部安全距离
|
||||
safeArea: {
|
||||
height: 32,
|
||||
},
|
||||
});
|
||||
205
components/medications/ExpiryDatePickerModal.tsx
Normal file
205
components/medications/ExpiryDatePickerModal.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Alert, Modal, Platform, Pressable, StyleSheet, View } from 'react-native';
|
||||
|
||||
interface ExpiryDatePickerModalProps {
|
||||
visible: boolean;
|
||||
currentDate: Date | null;
|
||||
onClose: () => void;
|
||||
onConfirm: (date: Date) => void;
|
||||
isAiDraft?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 有效期日期选择器组件
|
||||
*
|
||||
* 功能:
|
||||
* - 显示日期选择器弹窗
|
||||
* - 验证日期不能早于今天
|
||||
* - iOS 显示内联日历,Android 显示原生对话框
|
||||
* - 支持取消和确认操作
|
||||
*/
|
||||
export function ExpiryDatePickerModal({
|
||||
visible,
|
||||
currentDate,
|
||||
onClose,
|
||||
onConfirm,
|
||||
isAiDraft = false,
|
||||
}: ExpiryDatePickerModalProps) {
|
||||
const { t } = useI18n();
|
||||
const scheme = (useColorScheme() ?? 'light') as keyof typeof Colors;
|
||||
const colors = Colors[scheme];
|
||||
|
||||
// 内部状态:选择的日期值
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(currentDate || new Date());
|
||||
|
||||
// 当弹窗显示时,同步当前日期
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setSelectedDate(currentDate || new Date());
|
||||
}
|
||||
}, [visible, currentDate]);
|
||||
|
||||
/**
|
||||
* 处理日期变化
|
||||
* iOS: 实时更新选择的日期
|
||||
* Android: 在用户点击确定时直接确认
|
||||
*/
|
||||
const handleDateChange = useCallback(
|
||||
(event: any, date?: Date) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
// iOS: 实时更新内部状态
|
||||
if (date) {
|
||||
setSelectedDate(date);
|
||||
}
|
||||
} else {
|
||||
// Android: 处理用户操作
|
||||
if (event.type === 'set' && date) {
|
||||
// 用户点击确定
|
||||
validateAndConfirm(date);
|
||||
} else {
|
||||
// 用户点击取消
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
/**
|
||||
* 验证并确认日期
|
||||
*/
|
||||
const validateAndConfirm = useCallback(
|
||||
(dateToConfirm: Date) => {
|
||||
// 验证有效期不能早于今天
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const selected = new Date(dateToConfirm);
|
||||
selected.setHours(0, 0, 0, 0);
|
||||
|
||||
if (selected < today) {
|
||||
Alert.alert('日期无效', '有效期不能早于今天');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查日期是否真的发生了变化
|
||||
const currentExpiry = currentDate ? dayjs(currentDate).format('YYYY-MM-DD') : null;
|
||||
const newExpiry = dayjs(dateToConfirm).format('YYYY-MM-DD');
|
||||
|
||||
if (currentExpiry === newExpiry) {
|
||||
// 日期没有变化,直接关闭
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
// 日期有效且发生了变化,执行确认回调
|
||||
onConfirm(dateToConfirm);
|
||||
onClose();
|
||||
},
|
||||
[currentDate, onClose, onConfirm]
|
||||
);
|
||||
|
||||
/**
|
||||
* iOS 平台的确认按钮处理
|
||||
*/
|
||||
const handleIOSConfirm = useCallback(() => {
|
||||
validateAndConfirm(selectedDate);
|
||||
}, [selectedDate, validateAndConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<Pressable style={styles.backdrop} onPress={onClose} />
|
||||
<View style={[styles.sheet, { backgroundColor: colors.surface }]}>
|
||||
<ThemedText style={[styles.title, { color: colors.text }]}>
|
||||
选择有效期
|
||||
</ThemedText>
|
||||
|
||||
<DateTimePicker
|
||||
value={selectedDate}
|
||||
mode="date"
|
||||
display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
|
||||
minimumDate={new Date()}
|
||||
onChange={handleDateChange}
|
||||
locale="zh-CN"
|
||||
/>
|
||||
|
||||
{/* iOS 平台显示确认和取消按钮 */}
|
||||
{Platform.OS === 'ios' && (
|
||||
<View style={styles.actions}>
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
style={[styles.btn, { borderColor: colors.border }]}
|
||||
>
|
||||
<ThemedText style={[styles.btnText, { color: colors.textSecondary }]}>
|
||||
{t('medications.detail.pickers.cancel')}
|
||||
</ThemedText>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={handleIOSConfirm}
|
||||
style={[styles.btn, styles.btnPrimary, { backgroundColor: colors.primary }]}
|
||||
>
|
||||
<ThemedText style={[styles.btnText, { color: colors.onPrimary }]}>
|
||||
{t('medications.detail.pickers.confirm')}
|
||||
</ThemedText>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(15, 23, 42, 0.4)',
|
||||
},
|
||||
sheet: {
|
||||
position: 'absolute',
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 40,
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 20,
|
||||
elevation: 8,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
marginBottom: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginTop: 16,
|
||||
},
|
||||
btn: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
},
|
||||
btnPrimary: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
btnText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
265
components/medications/MedicationPhotoGuideModal.tsx
Normal file
265
components/medications/MedicationPhotoGuideModal.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user