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

@@ -20,6 +20,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Keyboard,
KeyboardAvoidingView,
Modal,
Platform,
@@ -31,6 +32,7 @@ import {
TouchableOpacity,
View
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface UserProfile {
@@ -80,8 +82,9 @@ export default function EditProfileScreen() {
const [pickerDate, setPickerDate] = useState<Date>(new Date());
const [editingField, setEditingField] = useState<string | null>(null);
const [tempValue, setTempValue] = useState<string>('');
// 输入框字符串
// 键盘高度状态
const [keyboardHeight, setKeyboardHeight] = useState(0);
// 从本地存储加载(身高/体重等本地字段)
const loadLocalProfile = async () => {
@@ -128,6 +131,34 @@ export default function EditProfileScreen() {
loadLocalProfile();
}, []);
// 键盘事件监听器 - 只在名称和体重输入框显示时监听
useEffect(() => {
// 只有在编辑名称或体重字段时才需要监听键盘(这两个字段使用 TextInput
const needsKeyboardHandling = editingField === 'name' || editingField === 'weight';
if (!needsKeyboardHandling) {
setKeyboardHeight(0);
return;
}
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
const handleShow = (event: any) => {
const height = event?.endCoordinates?.height ?? 0;
setKeyboardHeight(height);
};
const handleHide = () => setKeyboardHeight(0);
const showSub = Keyboard.addListener(showEvent, handleShow);
const hideSub = Keyboard.addListener(hideEvent, handleHide);
return () => {
showSub.remove();
hideSub.remove();
};
}, [editingField]);
// 获取最大心率数据
useEffect(() => {
const loadMaximumHeartRate = async () => {
@@ -439,6 +470,7 @@ export default function EditProfileScreen() {
field={editingField}
value={tempValue}
profile={profile}
keyboardHeight={keyboardHeight}
onClose={() => {
setEditingField(null);
setTempValue('');
@@ -557,11 +589,12 @@ function ProfileCard({ icon, iconUri, iconColor, title, value, onPress, disabled
);
}
function EditModal({ visible, field, value, profile, onClose, onSave, colors, textColor, placeholderColor, t }: {
function EditModal({ visible, field, value, profile, keyboardHeight, onClose, onSave, colors, textColor, placeholderColor, t }: {
visible: boolean;
field: string | null;
value: string;
profile: UserProfile;
keyboardHeight: number;
onClose: () => void;
onSave: (field: string, value: string) => void;
colors: any;
@@ -569,6 +602,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
placeholderColor: string;
t: (key: string) => string;
}) {
const insets = useSafeAreaInsets();
const [inputValue, setInputValue] = useState(value);
const [selectedGender, setSelectedGender] = useState(profile.gender || 'female');
const [selectedActivity, setSelectedActivity] = useState(profile.activityLevel || 1);
@@ -685,7 +719,10 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
<Pressable style={styles.modalBackdrop} onPress={onClose} />
<View style={styles.editModalSheet}>
<View style={[
styles.editModalSheet,
{ paddingBottom: Math.max(keyboardHeight, insets.bottom) + 12 }
]}>
<View style={styles.modalHandle} />
{renderContent()}
<View style={styles.modalButtons}>