feat(medication): 重构AI分析为结构化展示并支持喝水提醒个性化配置
- 将药品AI分析从Markdown流式输出重构为结构化数据展示(V2) - 新增适合人群、不适合人群、主要成分、副作用等分类卡片展示 - 优化AI分析UI布局,采用卡片式设计提升可读性 - 新增药品跳过功能,支持用户标记本次用药为已跳过 - 修复喝水提醒逻辑,支持用户开关控制和自定义时间段配置 - 优化个人资料编辑页面键盘适配,避免输入框被遮挡 - 统一API响应码处理,兼容200和0两种成功状态码 - 更新版本号至1.0.28 BREAKING CHANGE: 药品AI分析接口从流式Markdown输出改为结构化JSON格式,旧版本分析结果将不再显示
This commit is contained in:
@@ -23,6 +23,7 @@ import { createWaterRecordAction } from '@/store/waterSlice';
|
||||
import { loadActiveFastingSchedule } from '@/utils/fasting';
|
||||
import { initializeHealthPermissions } from '@/utils/health';
|
||||
import { MoodNotificationHelpers, NutritionNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
import { getWaterReminderSettings } from '@/utils/userPreferences';
|
||||
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
|
||||
import React, { useEffect } from 'react';
|
||||
import { AppState, AppStateStatus } from 'react-native';
|
||||
@@ -228,10 +229,23 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
logger.info('✅ 心情提醒已注册')
|
||||
),
|
||||
|
||||
// 喝水提醒
|
||||
WaterNotificationHelpers.scheduleRegularWaterReminders(profile.name || '用户').then(() =>
|
||||
logger.info('✅ 喝水提醒已注册')
|
||||
),
|
||||
// 喝水提醒 - 需要先检查设置
|
||||
getWaterReminderSettings().then(settings => {
|
||||
if (settings.enabled) {
|
||||
// 如果使用的是自定义提醒,scheduleCustomWaterReminders 会被调用(通常在设置页面保存时)
|
||||
// 但为了保险起见,这里也可以根据设置类型来决定调用哪个
|
||||
// 目前逻辑似乎是 scheduleRegularWaterReminders 是默认的/旧的逻辑?
|
||||
// 查看 notificationHelpers.ts,scheduleRegularWaterReminders 是每2小时一次的固定逻辑
|
||||
// 而 scheduleCustomWaterReminders 是根据用户设置的时间段和间隔
|
||||
|
||||
// 如果用户开启了提醒,应该使用 scheduleCustomWaterReminders
|
||||
WaterNotificationHelpers.scheduleCustomWaterReminders(profile.name || '用户', settings).then(() =>
|
||||
logger.info('✅ 自定义喝水提醒已注册')
|
||||
);
|
||||
} else {
|
||||
logger.info('ℹ️ 用户未开启喝水提醒,跳过注册');
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
// 检查断食通知(如果有活跃计划)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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}>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -472,8 +472,16 @@ const medicationsResources = {
|
||||
action: {
|
||||
takeNow: '立即服用',
|
||||
taken: '已服用',
|
||||
skipped: '已跳过',
|
||||
skip: '跳过',
|
||||
submitting: '提交中...',
|
||||
},
|
||||
skipAlert: {
|
||||
title: '确认跳过',
|
||||
message: '确定要跳过本次用药吗?\n\n跳过后将不会记录为已服用。',
|
||||
cancel: '取消',
|
||||
confirm: '确认跳过',
|
||||
},
|
||||
earlyTakeAlert: {
|
||||
title: '尚未到服药时间',
|
||||
message: '该用药计划在 {{time}},现在还早于1小时以上。\n\n是否确认已服用此药物?',
|
||||
@@ -485,6 +493,11 @@ const medicationsResources = {
|
||||
message: '记录服药时发生错误,请稍后重试',
|
||||
confirm: '确定',
|
||||
},
|
||||
skipError: {
|
||||
title: '操作失败',
|
||||
message: '跳过操作失败,请稍后重试',
|
||||
confirm: '确定',
|
||||
},
|
||||
},
|
||||
// 添加药物页面翻译
|
||||
add: {
|
||||
@@ -1225,8 +1238,16 @@ const resources = {
|
||||
action: {
|
||||
takeNow: 'Take Now',
|
||||
taken: 'Taken',
|
||||
skipped: 'Skipped',
|
||||
skip: 'Skip',
|
||||
submitting: 'Submitting...',
|
||||
},
|
||||
skipAlert: {
|
||||
title: 'Confirm Skip',
|
||||
message: 'Are you sure you want to skip this medication?\n\nIt will not be recorded as taken.',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm Skip',
|
||||
},
|
||||
earlyTakeAlert: {
|
||||
title: 'Not yet time to take medication',
|
||||
message: 'This medication is scheduled for {{time}}, which is more than 1 hour from now.\n\nHave you already taken this medication?',
|
||||
@@ -1238,6 +1259,11 @@ const resources = {
|
||||
message: 'An error occurred while recording medication, please try again later',
|
||||
confirm: 'OK',
|
||||
},
|
||||
skipError: {
|
||||
title: 'Operation Failed',
|
||||
message: 'Skip operation failed, please try again later',
|
||||
confirm: 'OK',
|
||||
},
|
||||
},
|
||||
// 添加药物页面翻译
|
||||
add: {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.27</string>
|
||||
<string>1.0.28</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -144,7 +144,7 @@ async function doFetch<T>(path: string, options: ApiRequestOptions = {}): Promis
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (json.code !== undefined && json.code !== 0) {
|
||||
if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
|
||||
const errorMessage = (json && (json.message || json.error)) || `HTTP ${response.status}`;
|
||||
const error = new Error(errorMessage);
|
||||
// @ts-expect-error augment
|
||||
@@ -324,4 +324,3 @@ export async function postTextStream(path: string, body: any, callbacks: TextStr
|
||||
|
||||
return { abort, requestId };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { listChallenges } from '@/services/challengesApi';
|
||||
import { resyncFastingNotifications } from '@/services/fastingNotifications';
|
||||
import { store } from '@/store';
|
||||
import { selectActiveFastingPlan, selectActiveFastingSchedule } from '@/store/fastingSlice';
|
||||
import { getWaterIntakeFromHealthKit } from '@/utils/health';
|
||||
import AsyncStorage from '@/utils/kvStore';
|
||||
import { log } from '@/utils/logger';
|
||||
import { ChallengeNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
import { getWaterGoalFromStorage } from '@/utils/userPreferences';
|
||||
import { resyncFastingNotifications } from '@/services/fastingNotifications';
|
||||
import { selectActiveFastingSchedule, selectActiveFastingPlan } from '@/store/fastingSlice';
|
||||
import { getWaterGoalFromStorage, getWaterReminderEnabled } from '@/utils/userPreferences';
|
||||
import dayjs from 'dayjs';
|
||||
import * as BackgroundTask from 'expo-background-task';
|
||||
import * as TaskManager from 'expo-task-manager';
|
||||
@@ -33,6 +33,13 @@ async function executeWaterReminderTask(): Promise<void> {
|
||||
try {
|
||||
console.log('执行喝水提醒后台任务...');
|
||||
|
||||
// 检查是否开启了喝水提醒
|
||||
const isEnabled = await getWaterReminderEnabled();
|
||||
if (!isEnabled) {
|
||||
console.log('喝水提醒未开启,跳过后台任务');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前状态,添加错误处理
|
||||
let state;
|
||||
try {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getWaterIntakeFromHealthKit } from '@/utils/health';
|
||||
import AsyncStorage from '@/utils/kvStore';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { ChallengeNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
import { getWaterGoalFromStorage } from '@/utils/userPreferences';
|
||||
import { getWaterGoalFromStorage, getWaterReminderEnabled } from '@/utils/userPreferences';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -39,6 +39,13 @@ async function executeWaterReminderTask(): Promise<void> {
|
||||
try {
|
||||
console.log('执行喝水提醒后台任务...');
|
||||
|
||||
// 检查是否开启了喝水提醒
|
||||
const isEnabled = await getWaterReminderEnabled();
|
||||
if (!isEnabled) {
|
||||
console.log('喝水提醒未开启,跳过后台任务');
|
||||
return;
|
||||
}
|
||||
|
||||
let state;
|
||||
try {
|
||||
state = store.getState();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import type {
|
||||
DailyMedicationStats,
|
||||
Medication,
|
||||
MedicationAiAnalysisV2,
|
||||
MedicationForm,
|
||||
MedicationRecord,
|
||||
MedicationStatus,
|
||||
@@ -328,4 +329,18 @@ export async function analyzeMedicationStream(
|
||||
callbacks,
|
||||
{ timeoutMs: 120000 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取药品 AI 分析 V2 结构化报告
|
||||
* @param medicationId 药品 ID
|
||||
* @returns 结构化 AI 分析结果
|
||||
*/
|
||||
export async function analyzeMedicationV2(
|
||||
medicationId: string
|
||||
): Promise<MedicationAiAnalysisV2> {
|
||||
return api.post<MedicationAiAnalysisV2>(
|
||||
`/api/medications/${medicationId}/ai-analysis/v2`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,4 +91,17 @@ export interface MedicationDisplayItem {
|
||||
image?: any; // 图片资源
|
||||
recordId?: string; // 服药记录ID(用于更新状态)
|
||||
medicationId: string; // 药物ID
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 药品 AI 分析 V2 结构化数据
|
||||
*/
|
||||
export interface MedicationAiAnalysisV2 {
|
||||
suitableFor: string[]; // 适合人群
|
||||
unsuitableFor: string[]; // 不适合人群/慎用
|
||||
mainIngredients: string[]; // 主要成分
|
||||
mainUsage: string; // 主要用途/功效
|
||||
sideEffects: string[]; // 常见副作用
|
||||
storageAdvice: string[]; // 储存建议
|
||||
healthAdvice: string[]; // 健康建议/使用建议
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { NotificationData, NotificationTypes, notificationService } from '../services/notifications';
|
||||
import { getNotificationEnabled } from './userPreferences';
|
||||
import { getNotificationEnabled, getWaterReminderEnabled } from './userPreferences';
|
||||
|
||||
/**
|
||||
* 构建 coach 页面的深度链接
|
||||
@@ -433,6 +433,13 @@ export class WaterNotificationHelpers {
|
||||
currentHour: number = new Date().getHours()
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// 首先检查用户是否启用了喝水提醒
|
||||
const isWaterReminderEnabled = await getWaterReminderEnabled();
|
||||
if (!isWaterReminderEnabled) {
|
||||
console.log('用户未启用喝水提醒,跳过通知检查');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查时间限制:早上9点以前和晚上9点以后不通知
|
||||
if (currentHour < 9 || currentHour >= 23) {
|
||||
console.log(`当前时间${currentHour}点,不在通知时间范围内(9:00-21:00),跳过喝水提醒`);
|
||||
@@ -546,6 +553,15 @@ export class WaterNotificationHelpers {
|
||||
*/
|
||||
static async scheduleRegularWaterReminders(userName: string): Promise<string[]> {
|
||||
try {
|
||||
// 首先检查用户是否启用了喝水提醒
|
||||
const isWaterReminderEnabled = await getWaterReminderEnabled();
|
||||
if (!isWaterReminderEnabled) {
|
||||
console.log('用户未启用喝水提醒,不安排定期提醒');
|
||||
// 确保取消任何可能存在的旧提醒
|
||||
await this.cancelAllWaterReminders();
|
||||
return [];
|
||||
}
|
||||
|
||||
const notificationIds: string[] = [];
|
||||
|
||||
// 检查是否已经存在定期喝水提醒
|
||||
|
||||
Reference in New Issue
Block a user