feat(medication): 重构AI分析为结构化展示并支持喝水提醒个性化配置

- 将药品AI分析从Markdown流式输出重构为结构化数据展示(V2)
- 新增适合人群、不适合人群、主要成分、副作用等分类卡片展示
- 优化AI分析UI布局,采用卡片式设计提升可读性
- 新增药品跳过功能,支持用户标记本次用药为已跳过
- 修复喝水提醒逻辑,支持用户开关控制和自定义时间段配置
- 优化个人资料编辑页面键盘适配,避免输入框被遮挡
- 统一API响应码处理,兼容200和0两种成功状态码
- 更新版本号至1.0.28

BREAKING CHANGE: 药品AI分析接口从流式Markdown输出改为结构化JSON格式,旧版本分析结果将不再显示
This commit is contained in:
richarjiang
2025-11-20 10:10:53 +08:00
parent b36922756d
commit 84abfa2506
12 changed files with 913 additions and 293 deletions

View File

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