From 70ecfee0427e4623b34e9be93f96368b80e31d19 Mon Sep 17 00:00:00 2001 From: 15945162479 <15945162479@qq.com> Date: Sat, 13 Dec 2025 14:46:15 +0800 Subject: [PATCH] Add File --- src/elderly/pages/HomePage.tsx | 998 +++++++++++++++++++++++++++++++++ 1 file changed, 998 insertions(+) create mode 100644 src/elderly/pages/HomePage.tsx diff --git a/src/elderly/pages/HomePage.tsx b/src/elderly/pages/HomePage.tsx new file mode 100644 index 0000000..3555c43 --- /dev/null +++ b/src/elderly/pages/HomePage.tsx @@ -0,0 +1,998 @@ +import React, { useState, useEffect } from 'react'; +import { Mic, Users, Image } from 'lucide-react'; +import { AvatarStage } from '../components/AvatarStage'; +import { MedicationCard } from '../components/MedicationCard'; +import { MoodBoard } from '../components/MoodBoard'; +import { MemoryPlayer } from '../components/MemoryPlayer'; +import { MediaPlayer } from '../components/MediaPlayer'; +import { EmergencySheet } from '../components/EmergencySheet'; +import { ScheduleList } from '../components/ScheduleList'; +import { ScheduleReminderToast } from '../components/ScheduleReminderToast'; +import { ToastMessage } from '../components/ToastMessage'; +import { ConfirmDialog } from '../components/ConfirmDialog'; +import { TransparentMediaOverlay } from '../components/TransparentMediaOverlay'; +import { LogNotification } from '../components/LogNotification'; +import * as scheduleService from '../services/scheduleService'; +import * as mediaService from '../services/mediaService'; +import * as messageService from '../services/messageService'; +import * as alertService from '../services/alertService'; +import * as moodService from '../../family/services/moodService'; +import { useToastSSE } from '../hooks/useToastSSE'; + +/** + * 老人端主页 + * 优化为 9:16 竖屏使用(如平板竖屏) + * 包含数字人、大按钮和各类卡片叠加层 + */ +export const HomePage: React.FC = () => { + const [activeOverlay, setActiveOverlay] = useState< + 'none' | 'medication' | 'mood' | 'memory' | 'emergency' | 'schedule' | 'media' + >('none'); + const [isAvatarActive, setIsAvatarActive] = useState(false); + const [memoryMode, setMemoryMode] = useState<'pip' | 'fullscreen'>('fullscreen'); + const [toastMessage, setToastMessage] = useState<{ type: 'success' | 'info' | 'calling'; message: string } | null>(null); + const [confirmDialog, setConfirmDialog] = useState<{ message: string; onConfirm: () => void } | null>(null); + const [currentMediaIndex, setCurrentMediaIndex] = useState(0); + const [sdkStatus, setSDKStatus] = useState<'loading' | 'ready' | 'error' | 'config-missing'>('loading'); + const [wsStatus, setWSStatus] = useState<'disconnected' | 'connecting' | 'connected'>('disconnected'); + const [todaySchedules, setTodaySchedules] = useState([]); + const [reminderSchedule, setReminderSchedule] = useState(null); + const [shownReminders, setShownReminders] = useState>(new Set()); + const [postponedReminders, setPostponedReminders] = useState>(new Map()); // 记录推迟的日程和推迟到的时间 + const [recommendedMedia, setRecommendedMedia] = useState([]); + const [loadingMedia, setLoadingMedia] = useState(false); + const [playedMessages, setPlayedMessages] = useState>(new Set()); // 记录已播报的留言ID + const [mediaOverlay, setMediaOverlay] = useState<{ + filename: string; + type: 'photo' | 'video'; + text?: string; + duration?: number; + } | null>(null); // 透明窗口媒体展示状态 + const [isMicrophoneEnabled, setIsMicrophoneEnabled] = useState(true); // 麦克风状态 + const [currentMood, setCurrentMood] = useState(null); // 当前情绪 + const [logMessage, setLogMessage] = useState(null); // WebSocket log消息 + const familyId = 'family_001'; // 实际使用时从用户上下文获取 + const elderlyId = 1; // 老人用户ID,实际使用时从用户上下文获取 + + // 轮询检查联系家人的alerts(与家属端相同的方案) + useEffect(() => { + let lastAlertId = 0; // 记录上次处理的alert ID + let isInitialized = false; // 标记是否已初始化 + + const checkContactFamilyAlerts = async () => { + try { + const response = await fetch( + `http://localhost:8000/api/family/alerts?family_id=${familyId}&handled=false&alert_type=contact_family&limit=1` + ); + + if (!response.ok) { + console.error('[HomePage] 查询alerts失败:', response.status); + return; + } + + const data = await response.json(); + const alerts = data.alerts || []; + + if (alerts.length > 0) { + const alert = alerts[0]; + + // 首次初始化:只记录ID,不显示Toast(避免刷新页面时重复显示旧alert) + if (!isInitialized) { + lastAlertId = alert.id; + isInitialized = true; + console.log('[HomePage] 初始化lastAlertId:', lastAlertId); + return; + } + + // 只处理新的alert(避免重复显示) + if (alert.id > lastAlertId) { + lastAlertId = alert.id; + + console.log('[HomePage] ✓ 收到联系家人alert:', alert); + + // 显示Toast + const message = alert.metadata?.is_emergency + ? "正在紧急通知家人..." + : "正在通知家人..."; + + setToastMessage({ + type: 'calling', + message: message + }); + console.log('[HomePage] ✓ Toast已显示:', message); + + // 10秒后自动关闭 + setTimeout(() => { + console.log('[HomePage] 关闭Toast'); + setToastMessage(null); + }, 10000); + } + } else { + // 没有未处理的alert时,标记为已初始化 + if (!isInitialized) { + isInitialized = true; + console.log('[HomePage] 初始化完成,当前无未处理alert'); + } + } + } catch (error) { + console.error('[HomePage] 检查alerts失败:', error); + } + }; + + // 立即执行一次 + checkContactFamilyAlerts(); + + // 每2秒检查一次新alerts + const interval = setInterval(checkContactFamilyAlerts, 2000); + + return () => clearInterval(interval); + }, [familyId]); + + // 加载今日日程 + useEffect(() => { + loadTodaySchedules(); + // 每分钟刷新一次 + const interval = setInterval(loadTodaySchedules, 60000); + return () => clearInterval(interval); + }, []); + + // 监听日程状态变化,自动关闭已完成/已忽略的弹窗 + useEffect(() => { + if (reminderSchedule && reminderSchedule.id) { + // 在最新的日程列表中查找当前弹窗对应的日程 + const updatedSchedule = todaySchedules.find(s => s.id === reminderSchedule.id); + // 如果日程状态不是pending,关闭弹窗 + if (updatedSchedule && updatedSchedule.status !== 'pending') { + console.log(`日程 ${reminderSchedule.title} 状态已变更为 ${updatedSchedule.status},关闭弹窗`); + setReminderSchedule(null); + } + } + }, [todaySchedules, reminderSchedule]); + + + // 加载当前情绪 + const loadCurrentMood = async () => { + try { + const response = await moodService.getFamilyMoods(familyId, { limit: 1 }); + if (response.records && response.records.length > 0) { + setCurrentMood(response.records[0].mood_type); + } + } catch (error) { + console.error('加载当前情绪失败:', error); + } + }; + + // 初始化加载当前情绪 + useEffect(() => { + loadCurrentMood(); + }, []); + + // 加载推荐媒体 + const loadRecommendedMedia = async () => { + try { + setLoadingMedia(true); + const media = await mediaService.getRecommendedMedia(familyId, elderlyId); + setRecommendedMedia(media); + console.log('加载到推荐媒体:', media.length, '个'); + } catch (error) { + console.error('加载推荐媒体失败:', error); + } finally { + setLoadingMedia(false); + } + }; + + + // 检测日程到达时间 + useEffect(() => { + const checkScheduleTime = () => { + const now = new Date(); + + // 遍历今日日程,查找需要提醒的 + todaySchedules.forEach((schedule) => { + if (!schedule.id) return; + + // 只处理待执行状态的日程 + if (schedule.status !== 'pending') return; + + // 已经提醒过的跳过 + if (shownReminders.has(schedule.id)) return; + + // 检查是否被推迟,如果被推迟且未到推迟时间,则跳过 + const postponedTime = postponedReminders.get(schedule.id); + if (postponedTime && now < postponedTime) { + console.log(`日程 ${schedule.title} 已推迟到 ${postponedTime.toLocaleTimeString()},暂不提醒`); + return; + } + + // 如果已过推迟时间,清除推迟记录 + if (postponedTime && now >= postponedTime) { + console.log(`日程 ${schedule.title} 推迟时间已到,现在提醒`); + setPostponedReminders(prev => { + const newMap = new Map(prev); + newMap.delete(schedule.id!); + return newMap; + }); + } + + const scheduleTime = new Date(schedule.schedule_time); + const diffMinutes = (scheduleTime.getTime() - now.getTime()) / (1000 * 60); + + // 到达时间(允许 1 分钟误差) + if (diffMinutes <= 1 && diffMinutes >= -1) { + console.log('日程到达时间,显示提醒:', schedule.title); + + // 构建提醒内容 + const timeStr = scheduleService.formatTime(schedule.schedule_time); + const typeLabel = scheduleService.getScheduleTypeLabel(schedule.schedule_type || 'other'); + let reminderText = `${timeStr},${typeLabel}提醒:${schedule.title}`; + + // 如果有描述,添加描述 + if (schedule.description) { + reminderText += `。${schedule.description}`; + } + + // 添加操作询问 + reminderText += `。请问您要标记完成、推迟执行,还是忽略行程?`; + + // 推送播报内容到数字人 + sendToAvatar(reminderText); + + // 显示弹窗提醒 + setReminderSchedule(schedule); + setShownReminders(prev => new Set(prev).add(schedule.id!)); + } + }); + }; + + // 每 10 秒检查一次 + const interval = setInterval(checkScheduleTime, 10000); + checkScheduleTime(); // 立即执行一次 + + return () => clearInterval(interval); + }, [todaySchedules, shownReminders, postponedReminders]); + + const loadTodaySchedules = async () => { + try { + const now = new Date().toLocaleTimeString('zh-CN'); + console.log(`[${now}] 自动检查日程更新...`); + const data = await scheduleService.getTodaySchedules(familyId); + setTodaySchedules(data); + console.log(`[${now}] 加载到 ${data.length} 条日程`); + } catch (error) { + console.error('加载今日日程失败:', error); + } + }; + + // 检查并播报待播放的留言 + const checkAndPlayMessages = async () => { + try { + const now = new Date().toLocaleTimeString('zh-CN'); + console.log(`[${now}] 检查待播放留言...`); + + const pendingMessages = await messageService.getPendingMessages(familyId); + + if (pendingMessages.length > 0) { + console.log(`发现 ${pendingMessages.length} 条待播放留言`); + + for (const message of pendingMessages) { + // 检查是否已经播报过(避免重复播报) + if (!playedMessages.has(message.id)) { + console.log(`播报留言 ID: ${message.id} - 来自${message.sender_relation}${message.sender_name}`); + + // 推送到数字人播报 + await messageService.playMessageOnAvatar(message); + + // 显示Toast字幕提示(30秒) + const toastText = `来自${message.sender_relation}${message.sender_name}的留言:${message.content}`; + setToastMessage({ type: 'info', message: toastText }); + + // 30秒后自动关闭Toast + setTimeout(() => { + setToastMessage(null); + }, 30000); + + // 标记为已播放 + await messageService.markAsPlayed(message.id); + + // 记录已播报 + setPlayedMessages((prev) => new Set(prev).add(message.id)); + + console.log(`留言 ID: ${message.id} 播报完成`); + } + } + } + } catch (error) { + console.error('检查并播报留言失败:', error); + } + }; + + // 定时检查待播放留言(每分钟检查一次) + useEffect(() => { + checkAndPlayMessages(); // 立即执行一次 + const interval = setInterval(checkAndPlayMessages, 60000); // 每分钟检查 + return () => clearInterval(interval); + }, [playedMessages]); + + // 轮询媒体展示事件(每5秒检查一次) + const pollMediaEvents = async () => { + try { + const response = await fetch( + `http://localhost:8000/api/elderly/poll-media-events?family_id=${familyId}` + ); + + if (!response.ok) { + throw new Error(`轮询媒体事件失败: ${response.statusText}`); + } + + const data = await response.json(); + + if (data.event && data.event.metadata) { + const metadata = data.event.metadata; + console.log('收到媒体展示事件:', metadata); + + // 推送播报内容到数字人(与日程模块相同的逻辑) + if (metadata.avatar_text) { + console.log('准备调用 sendToAvatar,文本内容:', metadata.avatar_text); + + // 确保麦克风已开启(媒体展示时需要数字人播报) + await ensureMicrophoneEnabled(); + + await sendToAvatar(metadata.avatar_text); + console.log('sendToAvatar 调用完成'); + } else { + console.warn('没有 avatar_text,跳过数字人播报'); + } + + // 设置媒体展示状态,触发透明窗口弹出 + setMediaOverlay({ + filename: metadata.media_filename, + type: metadata.media_type || 'photo', + text: metadata.avatar_text, + duration: metadata.duration || 30, + }); + } + } catch (error) { + console.error('轮询媒体事件错误:', error); + } + }; + + // 定时轮询媒体展示事件(每5秒检查一次) + useEffect(() => { + pollMediaEvents(); // 立即执行一次 + const interval = setInterval(pollMediaEvents, 5000); // 每5秒检查 + return () => clearInterval(interval); + }, []); + + // 确保麦克风已开启 + const ensureMicrophoneEnabled = async () => { + try { + console.log('[ensureMicrophoneEnabled] 确保麦克风已开启...'); + const response = await fetch('http://127.0.0.1:5000/api/toggle-microphone', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ enabled: true }), + }); + + if (!response.ok) { + throw new Error(`开启麦克风失败: ${response.statusText}`); + } + + const result = await response.json(); + console.log('[ensureMicrophoneEnabled] 麦克风状态:', result); + } catch (error) { + console.error('[ensureMicrophoneEnabled] 开启麦克风错误:', error); + } + }; + + // 向数字人推送播报内容 + const sendToAvatar = async (text: string) => { + try { + console.log('[sendToAvatar] 开始推送播报内容:', text); + console.log('[sendToAvatar] 请求URL: http://127.0.0.1:5000/transparent-pass'); + + const requestBody = { + user: 'User', + text: text, + }; + console.log('[sendToAvatar] 请求体:', requestBody); + + const response = await fetch('http://127.0.0.1:5000/transparent-pass', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + console.log('[sendToAvatar] 响应状态:', response.status, response.statusText); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[sendToAvatar] 响应错误内容:', errorText); + throw new Error(`推送播报失败: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + console.log('[sendToAvatar] 播报内容推送成功,响应:', result); + } catch (error) { + console.error('[sendToAvatar] 推送播报内容错误:', error); + // 不抛出错误,让流程继续(即使播报失败,也要显示媒体) + } + }; + + // 模拟媒体库数据 + const mediaLibrary = [ + { id: '1', url: '/placeholder-photo.jpg', type: 'photo' as const, caption: '小米 2018 秋游' }, + { id: '2', url: '/placeholder-photo-2.jpg', type: 'photo' as const, caption: '2019 春节团聚' }, + { id: '3', url: '/placeholder-photo-3.jpg', type: 'photo' as const, caption: '奶奶80岁生日' }, + { id: '4', url: '/placeholder-photo-4.jpg', type: 'photo' as const, caption: '家庭野餐' }, + { id: '5', url: '/placeholder-photo-5.jpg', type: 'photo' as const, caption: '小孙子周岁' }, + ]; + + // 获取当前时间和日期信息 + const now = new Date(); + const currentTime = now.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + }); + const currentDate = now.toLocaleDateString('zh-CN', { + month: 'long', + day: 'numeric', + }); + const currentDay = now.toLocaleDateString('zh-CN', { + weekday: 'long', + }); + // 模拟天气信息(实际使用时应从天气API获取) + const weather = '晴 22°C'; + + // 切换麦克风开关 + const handleMicClick = async () => { + try { + console.log('[handleMicClick] 切换麦克风状态,当前状态:', isMicrophoneEnabled); + + // 先显示视觉反馈 + setIsAvatarActive(true); + setTimeout(() => setIsAvatarActive(false), 1000); + + // 调用麦克风切换API(不传参数则自动切换) + const response = await fetch('http://127.0.0.1:5000/api/toggle-microphone', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), // 不传参数,自动切换状态 + }); + + if (!response.ok) { + throw new Error(`切换麦克风失败: ${response.statusText}`); + } + + const result = await response.json(); + console.log('[handleMicClick] 麦克风切换结果:', result); + + // 更新UI状态 + setIsMicrophoneEnabled(result.enabled); + + } catch (error) { + console.error('[handleMicClick] 切换麦克风错误:', error); + } + }; + + const handleFamilyClick = async () => { + setToastMessage({ type: 'calling', message: '正在呼叫家人...' }); + // 推送普通消息到家属端 + try { + await alertService.sendContactFamilyAlert(familyId); + console.log('已通知家人'); + } catch (error) { + console.error('通知家人失败:', error); + } + }; + + const handleEmergencyClick = () => { + setActiveOverlay('emergency'); + }; + + const handlePhotosClick = async () => { + // 加载推荐媒体 + await loadRecommendedMedia(); + setActiveOverlay('media'); + }; + + const handleNextMedia = () => { + if (currentMediaIndex < mediaLibrary.length - 1) { + setCurrentMediaIndex(currentMediaIndex + 1); + } + }; + + const handlePreviousMedia = () => { + if (currentMediaIndex > 0) { + setCurrentMediaIndex(currentMediaIndex - 1); + } + }; + + const handleSelectMedia = (index: number) => { + setCurrentMediaIndex(index); + }; + + // 获取临近日程(前后30分钟内) + const getNextSchedule = () => { + if (todaySchedules.length === 0) return null; + + const now = new Date(); + const sorted = scheduleService.sortSchedulesByTime(todaySchedules); + + // 查找在时间窗口内的日程(前30分钟到后30分钟) + for (const schedule of sorted) { + const scheduleTime = new Date(schedule.schedule_time); + const diffMinutes = (scheduleTime.getTime() - now.getTime()) / (1000 * 60); + + // 如果在前后30分钟范围内,显示这个日程 + if (diffMinutes >= -30 && diffMinutes <= 30) { + return schedule; + } + + // 如果日程还在30分钟之后,也显示(即将到来) + if (diffMinutes > 30) { + return schedule; + } + } + + // 如果所有日程都已过期超过30分钟,不显示任何日程 + return null; + }; + + // 获取临近的药物提醒(前后30分钟内) + const getNearbyMedicationReminder = () => { + if (todaySchedules.length === 0) return null; + + const now = new Date(); + + // 查找前后30分钟内的药物提醒 + for (const schedule of todaySchedules) { + if (schedule.schedule_type !== 'medication') continue; + + const scheduleTime = new Date(schedule.schedule_time); + const diffMinutes = (scheduleTime.getTime() - now.getTime()) / (1000 * 60); + + // 如果在前后30分钟范围内,返回这个药物提醒 + if (diffMinutes >= -30 && diffMinutes <= 30) { + return schedule; + } + } + + return null; + }; + + const nextSchedule = getNextSchedule(); + const nearbyMedication = getNearbyMedicationReminder(); + + return ( +
+ {/* 主内容区域 - 满屏显示 */} +
+ {/* 数字人画面 - 全屏背景 */} +
+ +
+ + {/* PIP 模式的媒体播放器 */} + {activeOverlay === 'memory' && memoryMode === 'pip' && ( + console.log('Liked')} + onDislike={() => console.log('Disliked')} + onClose={() => setActiveOverlay('none')} + onToggleMode={() => setMemoryMode('fullscreen')} + onNext={handleNextMedia} + onPrevious={handlePreviousMedia} + onSelectMedia={handleSelectMedia} + /> + )} + + {/* 顶部状态栏 - 悬浮层 */} +
+
+ {/* 左侧区域:状态指示器 + 时间日期 */} +
+ {/* 连接状态指示器 - 竖排两个绿点 */} +
+ + +
+ + {/* 时间日期信息 */} +
+
+ + {currentTime} + + + {currentDate} + +
+
+ {currentDay} + ☀️ {weather} +
+
+
+ + {/* 中间Logo区域 */} +
+
+

KinEcho

+ 亲情回声 +
+

+ Bring family moments to life. / 把家人的声音带到眼前。 +

+
+ + {nextSchedule && ( + + )} +
+
+ + {/* 左侧按钮组 - 功能按钮垂直排列 */} +
+ {/* 麦克风按钮容器 - 用于承载log通知 */} +
+ + + {/* Log通知 - 从麦克风按钮向右延伸 */} + {logMessage && ( + setLogMessage(null)} + /> + )} +
+ + +
+ + {/* 右下角 - 紧急求助按钮 */} +
+ +
+ + {/* 叠加层 - 用药提醒 */} + {activeOverlay === 'medication' && ( + { + setActiveOverlay('none'); + setToastMessage({ type: 'success', message: '已记录服药' }); + }} + onSnooze={(mins) => { + setActiveOverlay('none'); + setToastMessage({ type: 'info', message: `将在 ${mins} 分钟后再次提醒` }); + }} + onSkip={() => { + setActiveOverlay('none'); + setConfirmDialog({ + message: '跳过服药可能影响健康,确定吗?', + onConfirm: () => { + setConfirmDialog(null); + setToastMessage({ type: 'info', message: '已记录跳过' }); + } + }); + }} + onInfo={() => { + setToastMessage({ type: 'info', message: '氯沙坦用于降血压,请随餐服用,避免空腹。' }); + }} + /> + )} + + {/* 叠加层 - 心情选择 */} + {activeOverlay === 'mood' && ( + { + console.log('Selected mood:', mood); + // 更新当前情绪显示 + setCurrentMood(mood as moodService.MoodType); + // 根据心情触发不同的回忆内容 + setActiveOverlay('memory'); + }} + onClose={() => setActiveOverlay('none')} + /> + )} + + {/* 叠加层 - 媒体播放(全屏) */} + {activeOverlay === 'memory' && memoryMode === 'fullscreen' && ( + console.log('Liked')} + onDislike={() => console.log('Disliked')} + onClose={() => setActiveOverlay('none')} + onToggleMode={() => setMemoryMode('pip')} + onNext={handleNextMedia} + onPrevious={handlePreviousMedia} + onSelectMedia={handleSelectMedia} + /> + )} + + {/* 叠加层 - 紧急求助 */} + {activeOverlay === 'emergency' && ( + { + setActiveOverlay('none'); + setToastMessage({ type: 'calling', message: '正在呼叫家人...' }); + // 推送SOS紧急消息到家属端 + try { + await alertService.sendSOSAlert(familyId); + console.log('已发送SOS紧急通知给家人'); + } catch (error) { + console.error('发送SOS紧急通知失败:', error); + } + }} + onContactEmergency={async () => { + setActiveOverlay('none'); + setToastMessage({ type: 'info', message: '暂未对接应急中心,已通知家人' }); + // 也推送SOS紧急消息到家属端 + try { + await alertService.sendSOSAlert(familyId); + console.log('已发送SOS紧急通知给家人'); + } catch (error) { + console.error('发送SOS紧急通知失败:', error); + } + }} + onClose={() => setActiveOverlay('none')} + /> + )} + + {/* 叠加层 - 智能媒体播放器 */} + {activeOverlay === 'media' && recommendedMedia.length > 0 && ( + setActiveOverlay('none')} + /> + )} + + {/* 叠加层 - 日程列表 */} + {activeOverlay === 'schedule' && ( + setActiveOverlay('none')} + /> + )} + + {/* Toast 消息提示 */} + {toastMessage && ( + setToastMessage(null)} + /> + )} + + {/* 确认对话框 */} + {confirmDialog && ( + setConfirmDialog(null)} + /> + )} + + {/* 透明窗口媒体展示 */} + {mediaOverlay && ( + setMediaOverlay(null)} + /> + )} + + {/* 日程提醒 Toast */} + {reminderSchedule && ( + { + try { + if (reminderSchedule.id) { + // 更新日程状态为已完成 + await scheduleService.updateScheduleStatus(reminderSchedule.id, 'completed'); + } + setReminderSchedule(null); + // 重新加载日程列表 + loadTodaySchedules(); + } catch (error) { + console.error('标记完成失败:', error); + } + }} + onSkip={async () => { + try { + if (reminderSchedule.id) { + // 更新日程状态为已忽略 + await scheduleService.updateScheduleStatus(reminderSchedule.id, 'skipped'); + } + setReminderSchedule(null); + // 重新加载日程列表 + loadTodaySchedules(); + } catch (error) { + console.error('忽略行程失败:', error); + } + }} + onDismiss={async () => { + if (!reminderSchedule) return; + + console.log('推迟执行日程:', reminderSchedule); + + try { + // 计算10分钟后的时间 + const postponedTime = new Date(); + postponedTime.setMinutes(postponedTime.getMinutes() + 10); + + // 如果是每日重复的日程,只推迟当前实例 + if (reminderSchedule.repeat_type === 'daily') { + // 记录推迟时间到 postponedReminders Map + setPostponedReminders(prev => { + const newMap = new Map(prev); + newMap.set(reminderSchedule.id!, postponedTime); + return newMap; + }); + // 从已显示列表中移除,允许再次提醒 + setShownReminders(prev => { + const newSet = new Set(prev); + if (reminderSchedule.id) newSet.delete(reminderSchedule.id); + return newSet; + }); + console.log(`每日重复日程已推迟到 ${postponedTime.toLocaleTimeString()},原日程保持不变`); + setReminderSchedule(null); + } else { + // 如果是一次性日程,更新数据库中的 schedule_time + const response = await fetch( + `http://localhost:8000/api/family/schedules/${reminderSchedule.id}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + family_id: reminderSchedule.family_id, + title: reminderSchedule.title, + description: reminderSchedule.description, + schedule_type: reminderSchedule.schedule_type, + schedule_time: postponedTime.toISOString(), + repeat_type: reminderSchedule.repeat_type, + repeat_days: reminderSchedule.repeat_days, + auto_remind: reminderSchedule.auto_remind, + status: 'pending', // 推迟后重置为pending状态 + }), + } + ); + + if (response.ok) { + console.log('一次性日程时间已更新为10分钟后:', postponedTime.toISOString()); + setReminderSchedule(null); + // 从已显示列表中移除,允许10分钟后再次提醒 + setShownReminders(prev => { + const newSet = new Set(prev); + if (reminderSchedule.id) newSet.delete(reminderSchedule.id); + return newSet; + }); + // 重新加载日程列表 + loadTodaySchedules(); + } else { + const errorText = await response.text(); + console.error('更新失败响应:', errorText); + throw new Error('更新日程时间失败'); + } + } + } catch (error) { + console.error('推迟执行失败:', error); + } + }} + onMissed={async () => { + try { + if (reminderSchedule?.id) { + console.log('30分钟无操作,自动标记为已错过:', reminderSchedule.title); + // 更新日程状态为已错过 + await scheduleService.updateScheduleStatus(reminderSchedule.id, 'missed'); + } + setReminderSchedule(null); + // 重新加载日程列表 + loadTodaySchedules(); + } catch (error) { + console.error('标记已错过失败:', error); + setReminderSchedule(null); + } + }} + onClose={() => { + // 手动关闭提醒(不做任何操作,只是隐藏) + setReminderSchedule(null); + }} + /> + )} + + {/* 调试按钮 - 方便演示 */} +
+ {/* 药物提醒按钮 - 只在前后30分钟有药物提醒时显示 */} + {nearbyMedication && ( + + )} + +
+
+
+ ); +};