This commit is contained in:
2025-12-13 14:46:20 +08:00
parent 42ccaa1265
commit 81306fdc04

View File

@@ -0,0 +1,415 @@
import React, { useState, useEffect } from 'react';
import {
AlertTriangle,
AlertCircle,
Clock,
Heart,
MessageCircle,
Send,
CheckCircle,
Phone,
} from 'lucide-react';
import * as messageService from '../services/messageService';
interface Alert {
id: string;
level: 'low' | 'medium' | 'high';
type: 'medication' | 'emotion' | 'inactive' | 'emergency' | 'sos_emergency' | 'contact_family';
message: string;
timestamp: string;
handled: boolean;
}
/**
* 家属端通知与远程关怀界面
* 显示通知时间线,支持发送祝福消息
*/
export const AlertsAndCare: React.FC = () => {
const [showMessageComposer, setShowMessageComposer] = useState(false);
const [messageText, setMessageText] = useState('');
const [deliveryTiming, setDeliveryTiming] = useState('asap');
const [alerts, setAlerts] = useState<Alert[]>([]);
const [loading, setLoading] = useState(true);
const [filterType, setFilterType] = useState<'all' | 'unhandled' | 'high'>('all');
const [stats, setStats] = useState<{
total: number;
unhandled: number;
high: number;
}>({ total: 0, unhandled: 0, high: 0 });
const familyId = 'family_001'; // 实际使用时从用户上下文获取
// 加载消息/告警数据
useEffect(() => {
loadAlerts();
loadStats();
// 每10秒轮询一次新消息
const interval = setInterval(() => {
loadAlerts();
loadStats();
}, 10000);
return () => clearInterval(interval);
}, [filterType]);
const loadStats = async () => {
try {
const statsData = await messageService.getAlertStats(familyId);
// 计算各个筛选项的数量
// total 应该是所有消息的总数,而不是今天的消息数
const totalFromLevels = Object.values(statsData.level_stats || {}).reduce((sum, count) => sum + count, 0);
const total = totalFromLevels;
const unhandled = statsData.status_stats?.unhandled || 0;
const high = statsData.level_stats?.high || 0;
setStats({ total, unhandled, high });
} catch (error) {
console.error('加载统计失败:', error);
}
};
const loadAlerts = async () => {
try {
setLoading(true);
// 根据筛选类型设置API参数
const options: any = {};
if (filterType === 'unhandled') {
options.handled = false;
} else if (filterType === 'high') {
options.level = 'high';
}
const { alerts: data } = await messageService.getFamilyAlerts(familyId, options);
// 转换数据格式
const convertedAlerts: Alert[] = data.map((alert) => ({
id: alert.id.toString(),
level: alert.level,
type: alert.alert_type,
message: alert.message,
timestamp: new Date(alert.created_at).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}),
handled: alert.handled,
}));
setAlerts(convertedAlerts);
} catch (error) {
console.error('加载消息失败:', error);
// 如果API失败使用模拟数据
setAlerts([
{
id: '1',
level: 'high',
type: 'emotion',
message: '检测到连续负面情绪,已触发安抚流程',
timestamp: '2023-11-13 14:30',
handled: false,
},
{
id: '2',
level: 'medium',
type: 'medication',
message: '晚药延迟 15 分钟服用',
timestamp: '2023-11-13 20:15',
handled: true,
},
{
id: '3',
level: 'low',
type: 'inactive',
message: '下午时段互动较少(仅 1 次对话)',
timestamp: '2023-11-13 17:00',
handled: true,
},
]);
} finally {
setLoading(false);
}
};
const getLevelConfig = (level: Alert['level']) => {
switch (level) {
case 'high':
return {
bg: 'bg-red-50',
border: 'border-red-400',
text: 'text-red-700',
icon: AlertTriangle,
label: '🚨 紧急',
};
case 'medium':
return {
bg: 'bg-orange-50',
border: 'border-orange-400',
text: 'text-orange-700',
icon: Phone,
label: '📞 普通',
};
case 'low':
return {
bg: 'bg-yellow-50',
border: 'border-yellow-400',
text: 'text-yellow-700',
icon: Clock,
label: '💡 提示',
};
}
};
const getTypeIcon = (type: Alert['type']) => {
switch (type) {
case 'sos_emergency':
return AlertTriangle;
case 'contact_family':
return Phone;
case 'emotion':
return Heart;
case 'medication':
return Clock;
case 'inactive':
return MessageCircle;
default:
return AlertCircle;
}
};
const handleMarkAsHandled = async (alertId: string) => {
try {
await messageService.handleAlert(Number(alertId));
// 重新加载数据和统计
await loadAlerts();
await loadStats();
} catch (error) {
console.error('标记为已处理失败:', error);
}
};
const handleSendMessage = () => {
console.log('Send message:', { messageText, deliveryTiming });
setShowMessageComposer(false);
setMessageText('');
};
// 显示加载状态
if (loading && alerts.length === 0) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto mb-4"></div>
<p className="text-gray-600">...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
{/* 顶部导航 */}
<div className="bg-white border-b">
<div className="px-4 py-3">
<div className="flex items-center justify-between mb-3">
<h1 className="text-xl font-bold text-gray-900"></h1>
</div>
{/* 筛选器 */}
<div className="flex gap-2 overflow-x-auto pb-2 -mb-2 scrollbar-hide">
<button
onClick={() => setFilterType('all')}
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
filterType === 'all'
? 'bg-primary-100 text-primary-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
({stats.total})
</button>
<button
onClick={() => setFilterType('unhandled')}
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
filterType === 'unhandled'
? 'bg-primary-100 text-primary-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
({stats.unhandled})
</button>
<button
onClick={() => setFilterType('high')}
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
filterType === 'high'
? 'bg-primary-100 text-primary-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
({stats.high})
</button>
</div>
</div>
</div>
{/* 主要内容区 - 手机优化 */}
<div className="px-4 py-4">
{/* 通知列表 */}
<div className="space-y-3">
{alerts.map((alert) => {
const config = getLevelConfig(alert.level);
const Icon = config.icon;
return (
<div
key={alert.id}
className={`p-4 border-l-4 rounded-lg ${config.bg} ${config.border}`}
>
<div className="flex items-start gap-3">
<Icon className={config.text} size={20} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className={`text-xs font-bold ${config.text} uppercase`}>
{config.label}
</span>
<span className="text-xs text-gray-500">
{alert.timestamp}
</span>
{alert.handled && (
<span className="text-xs text-green-600 flex items-center gap-1">
<CheckCircle size={14} />
</span>
)}
</div>
<p className={`text-sm ${config.text} font-medium mb-3`}>
{alert.message}
</p>
{!alert.handled && (
<div className="flex gap-2 flex-wrap">
<button
onClick={() => handleMarkAsHandled(alert.id)}
className="px-3 py-1.5 bg-white border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50 transition-colors"
>
</button>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
{/* 空状态 */}
{alerts.length === 0 && (
<div className="text-center py-12">
<AlertCircle size={48} className="mx-auto text-gray-400 mb-3" />
<p className="text-gray-500"></p>
</div>
)}
</div>
{/* 消息编辑器弹窗 */}
{showMessageComposer && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-6 animate-fade-in">
<div className="bg-white rounded-2xl max-w-2xl w-full p-8">
<h2 className="text-xl font-bold text-gray-900 mb-6"></h2>
{/* 消息类型选择 */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="flex gap-3">
<button className="flex-1 p-3 border-2 border-primary-600 bg-primary-50 text-primary-700 rounded-lg font-medium">
</button>
<button className="flex-1 p-3 border-2 border-gray-200 text-gray-700 rounded-lg font-medium hover:border-gray-300">
</button>
</div>
</div>
{/* 消息内容 */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<textarea
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
placeholder="请输入您想对老人说的话..."
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-none"
/>
</div>
{/* 投递时机 */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select
value={deliveryTiming}
onChange={(e) => setDeliveryTiming(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="asap"></option>
<option value="tonight"> 19:00 </option>
<option value="emotion"></option>
<option value="after_meal"></option>
</select>
<p className="mt-1 text-xs text-gray-500">
</p>
</div>
{/* 快捷短语 */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="flex flex-wrap gap-2">
{[
'记得按时吃药',
'多喝水',
'天气转凉,注意保暖',
'想你了',
'我爱你',
].map((phrase, index) => (
<button
key={index}
onClick={() => setMessageText(phrase)}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm hover:bg-gray-200 transition-colors"
>
{phrase}
</button>
))}
</div>
</div>
{/* 操作按钮 */}
<div className="flex gap-3">
<button
onClick={() => setShowMessageComposer(false)}
className="flex-1 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
</button>
<button
onClick={handleSendMessage}
disabled={!messageText.trim()}
className="flex-1 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<Send size={18} />
</button>
</div>
</div>
</div>
)}
</div>
);
};