feat(medication): 重构AI分析为结构化展示并支持喝水提醒个性化配置
- 将药品AI分析从Markdown流式输出重构为结构化数据展示(V2) - 新增适合人群、不适合人群、主要成分、副作用等分类卡片展示 - 优化AI分析UI布局,采用卡片式设计提升可读性 - 新增药品跳过功能,支持用户标记本次用药为已跳过 - 修复喝水提醒逻辑,支持用户开关控制和自定义时间段配置 - 优化个人资料编辑页面键盘适配,避免输入框被遮挡 - 统一API响应码处理,兼容200和0两种成功状态码 - 更新版本号至1.0.28 BREAKING CHANGE: 药品AI分析接口从流式Markdown输出改为结构化JSON格式,旧版本分析结果将不再显示
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { useAppDispatch } from '@/hooks/redux';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { takeMedicationAction } from '@/store/medicationsSlice';
|
||||
import { skipMedicationAction, takeMedicationAction } from '@/store/medicationsSlice';
|
||||
import type { MedicationDisplayItem } from '@/types/medication';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
@@ -100,6 +100,64 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理跳过操作
|
||||
*/
|
||||
const handleSkipMedication = async () => {
|
||||
// 检查 recordId 是否存在
|
||||
if (!medication.recordId || isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示二次确认弹窗
|
||||
Alert.alert(
|
||||
t('medications.card.skipAlert.title'),
|
||||
t('medications.card.skipAlert.message'),
|
||||
[
|
||||
{
|
||||
text: t('medications.card.skipAlert.cancel'),
|
||||
style: 'cancel',
|
||||
onPress: () => {
|
||||
console.log('用户取消跳过');
|
||||
},
|
||||
},
|
||||
{
|
||||
text: t('medications.card.skipAlert.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
executeSkipMedication(medication.recordId!);
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行跳过操作
|
||||
*/
|
||||
const executeSkipMedication = async (recordId: string) => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// 调用 Redux action 标记为已跳过
|
||||
await dispatch(skipMedicationAction({
|
||||
recordId: recordId,
|
||||
})).unwrap();
|
||||
|
||||
// 可选:显示成功提示
|
||||
// Alert.alert('跳过成功', '已跳过本次用药');
|
||||
} catch (error) {
|
||||
console.error('[MEDICATION_CARD] 跳过操作失败', error);
|
||||
Alert.alert(
|
||||
t('medications.card.skipError.title'),
|
||||
error instanceof Error ? error.message : t('medications.card.skipError.message'),
|
||||
[{ text: t('medications.card.skipError.confirm') }]
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderStatusBadge = () => {
|
||||
if (medication.status === 'missed') {
|
||||
return (
|
||||
@@ -136,6 +194,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
||||
};
|
||||
|
||||
const renderAction = () => {
|
||||
// 已服用状态
|
||||
if (medication.status === 'taken') {
|
||||
return (
|
||||
<View style={[styles.actionButton, styles.actionButtonTaken]}>
|
||||
@@ -145,32 +204,73 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
||||
);
|
||||
}
|
||||
|
||||
// 只要没有服药,都可以显示立即服用
|
||||
// 已跳过状态
|
||||
if (medication.status === 'skipped') {
|
||||
return (
|
||||
<View style={[styles.actionButton, styles.actionButtonSkipped]}>
|
||||
<Ionicons name="close-circle" size={18} color="#fff" />
|
||||
<ThemedText style={styles.actionButtonText}>{t('medications.card.action.skipped')}</ThemedText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 待服用或已错过状态,显示操作按钮
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={handleTakeMedication}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={[styles.actionButton, styles.actionButtonUpcoming]}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(19, 99, 255, 0.3)"
|
||||
isInteractive={!isSubmitting}
|
||||
>
|
||||
<ThemedText style={styles.actionButtonText}>
|
||||
{isSubmitting ? t('medications.card.action.submitting') : t('medications.card.action.takeNow')}
|
||||
</ThemedText>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.actionButton, styles.actionButtonUpcoming, styles.fallbackActionButton]}>
|
||||
<ThemedText style={styles.actionButtonText}>
|
||||
{isSubmitting ? t('medications.card.action.submitting') : t('medications.card.action.takeNow')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<View style={styles.actionButtonsRow}>
|
||||
{/* 跳过按钮 */}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={handleSkipMedication}
|
||||
disabled={isSubmitting}
|
||||
style={styles.skipButtonWrapper}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={[styles.actionButton, styles.actionButtonSkip]}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(156, 163, 175, 0.2)"
|
||||
isInteractive={!isSubmitting}
|
||||
>
|
||||
<ThemedText style={styles.actionButtonTextSkip}>
|
||||
{t('medications.card.action.skip')}
|
||||
</ThemedText>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.actionButton, styles.actionButtonSkip, styles.fallbackActionButtonSkip]}>
|
||||
<ThemedText style={styles.actionButtonTextSkip}>
|
||||
{t('medications.card.action.skip')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 立即服用按钮 */}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={handleTakeMedication}
|
||||
disabled={isSubmitting}
|
||||
style={styles.takeButtonWrapper}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={[styles.actionButton, styles.actionButtonUpcoming]}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(19, 99, 255, 0.3)"
|
||||
isInteractive={!isSubmitting}
|
||||
>
|
||||
<ThemedText style={styles.actionButtonText}>
|
||||
{isSubmitting ? t('medications.card.action.submitting') : t('medications.card.action.takeNow')}
|
||||
</ThemedText>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.actionButton, styles.actionButtonUpcoming, styles.fallbackActionButton]}>
|
||||
<ThemedText style={styles.actionButtonText}>
|
||||
{isSubmitting ? t('medications.card.action.submitting') : t('medications.card.action.takeNow')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -286,6 +386,16 @@ const styles = StyleSheet.create({
|
||||
actionContainer: {
|
||||
marginTop: 8,
|
||||
},
|
||||
actionButtonsRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
skipButtonWrapper: {
|
||||
flex: 1,
|
||||
},
|
||||
takeButtonWrapper: {
|
||||
flex: 2,
|
||||
},
|
||||
actionButton: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
@@ -302,6 +412,12 @@ const styles = StyleSheet.create({
|
||||
actionButtonTaken: {
|
||||
backgroundColor: '#1FBF4B',
|
||||
},
|
||||
actionButtonSkipped: {
|
||||
backgroundColor: '#9CA3AF',
|
||||
},
|
||||
actionButtonSkip: {
|
||||
backgroundColor: '#E5E7EB',
|
||||
},
|
||||
actionButtonMissed: {
|
||||
backgroundColor: '#9CA3AF',
|
||||
},
|
||||
@@ -310,6 +426,11 @@ const styles = StyleSheet.create({
|
||||
borderColor: 'rgba(19, 99, 255, 0.3)',
|
||||
backgroundColor: 'rgba(19, 99, 255, 0.9)',
|
||||
},
|
||||
fallbackActionButtonSkip: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(156, 163, 175, 0.2)',
|
||||
backgroundColor: 'rgba(229, 231, 235, 0.9)',
|
||||
},
|
||||
fallbackActionButtonMissed: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(156, 163, 175, 0.3)',
|
||||
@@ -320,6 +441,11 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
actionButtonTextSkip: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#6B7280',
|
||||
},
|
||||
actionButtonTextMissed: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
|
||||
Reference in New Issue
Block a user