Add File
This commit is contained in:
779
src/family/pages/CarePlan.tsx
Normal file
779
src/family/pages/CarePlan.tsx
Normal file
@@ -0,0 +1,779 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
X,
|
||||||
|
Clock,
|
||||||
|
AlertCircle,
|
||||||
|
Pill,
|
||||||
|
Heart,
|
||||||
|
Activity,
|
||||||
|
Utensils,
|
||||||
|
Droplets,
|
||||||
|
Moon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import * as scheduleService from '../services/scheduleService';
|
||||||
|
import { Toast, ToastType } from '../components/Toast';
|
||||||
|
import { ConfirmDialog } from '../components/ConfirmDialog';
|
||||||
|
|
||||||
|
type CareType = 'medication' | 'exercise' | 'meal' | 'hydration' | 'sleep' | 'checkup' | 'other';
|
||||||
|
|
||||||
|
interface CareTask {
|
||||||
|
id: string;
|
||||||
|
type: CareType;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
times: string[];
|
||||||
|
active: boolean;
|
||||||
|
autoRemind: boolean; // 数字人自动提醒
|
||||||
|
status?: 'pending' | 'completed' | 'skipped' | 'missed';
|
||||||
|
repeatType?: 'once' | 'daily' | 'weekly' | 'monthly';
|
||||||
|
// 用药专用字段
|
||||||
|
dosage?: string;
|
||||||
|
route?: string;
|
||||||
|
withFood?: boolean;
|
||||||
|
gracePeriod?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 家属端护理计划界面
|
||||||
|
* 管理老人的用药、饮食、运动等护理任务
|
||||||
|
*/
|
||||||
|
export const CarePlan: React.FC = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState<'all' | CareType>('all');
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [editingTask, setEditingTask] = useState<CareTask | null>(null);
|
||||||
|
const [formType, setFormType] = useState<CareType>('medication');
|
||||||
|
const [schedules, setSchedules] = useState<scheduleService.Schedule[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [apiConnected, setApiConnected] = useState(false);
|
||||||
|
const [formTitle, setFormTitle] = useState('');
|
||||||
|
const [formDescription, setFormDescription] = useState('');
|
||||||
|
const [formTime, setFormTime] = useState('08:00');
|
||||||
|
const [formRepeatDaily, setFormRepeatDaily] = useState(true);
|
||||||
|
const [formAutoRemind, setFormAutoRemind] = useState(true);
|
||||||
|
const [toast, setToast] = useState<{ message: string; type: ToastType } | null>(null);
|
||||||
|
const [confirmDialog, setConfirmDialog] = useState<{
|
||||||
|
message: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
} | null>(null);
|
||||||
|
const familyId = 'family_001'; // 实际使用时从用户上下文获取
|
||||||
|
|
||||||
|
// 加载日程数据
|
||||||
|
useEffect(() => {
|
||||||
|
loadSchedules();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadSchedules = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
console.log('开始加载日程,family_id:', familyId);
|
||||||
|
const data = await scheduleService.getFamilySchedules(familyId);
|
||||||
|
console.log('加载到的日程数据:', data);
|
||||||
|
setSchedules(data);
|
||||||
|
setApiConnected(true); // 标记 API 已连接
|
||||||
|
console.log('API 已连接,日程数量:', data.length);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载日程失败:', error);
|
||||||
|
setApiConnected(false);
|
||||||
|
// 只在第一次加载失败时提示
|
||||||
|
if (!apiConnected) {
|
||||||
|
setToast({
|
||||||
|
message: '加载日程失败,请检查服务器是否已启动',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将 Schedule 转换为 CareTask 格式(用于兼容现有 UI)
|
||||||
|
const convertScheduleToTask = (schedule: scheduleService.Schedule): CareTask => {
|
||||||
|
// 安全地提取时间部分
|
||||||
|
let time = '00:00';
|
||||||
|
try {
|
||||||
|
const dateTime = scheduleService.formatDateTime(new Date(schedule.schedule_time));
|
||||||
|
const parts = dateTime.split(' ');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
time = parts[1].slice(0, 5);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('时间格式转换失败:', schedule.schedule_time, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoRemind = schedule.auto_remind === 1;
|
||||||
|
console.log(`转换日程 "${schedule.title}": auto_remind=${schedule.auto_remind} → autoRemind=${autoRemind}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: schedule.id?.toString() || '',
|
||||||
|
type: (schedule.schedule_type || 'other') as CareType,
|
||||||
|
name: schedule.title,
|
||||||
|
description: schedule.description,
|
||||||
|
times: [time],
|
||||||
|
active: schedule.is_active === 1,
|
||||||
|
autoRemind: autoRemind,
|
||||||
|
status: schedule.status || 'pending',
|
||||||
|
repeatType: schedule.repeat_type || 'once',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟数据已移除 - 始终使用真实API数据
|
||||||
|
|
||||||
|
const careTypeConfig = {
|
||||||
|
medication: { label: '用药', icon: Pill, color: 'blue' },
|
||||||
|
exercise: { label: '运动', icon: Activity, color: 'green' },
|
||||||
|
meal: { label: '饮食', icon: Utensils, color: 'orange' },
|
||||||
|
hydration: { label: '饮水', icon: Droplets, color: 'cyan' },
|
||||||
|
sleep: { label: '睡眠', icon: Moon, color: 'purple' },
|
||||||
|
checkup: { label: '检查', icon: Heart, color: 'red' },
|
||||||
|
other: { label: '其他', icon: Clock, color: 'gray' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据护理类型获取提示词
|
||||||
|
const getPlaceholders = (type: CareType) => {
|
||||||
|
const placeholders: Record<CareType, { title: string; description: string }> = {
|
||||||
|
medication: {
|
||||||
|
title: '例如:氯沙坦、阿司匹林',
|
||||||
|
description: '例如:饭后服用,每日一次,50mg'
|
||||||
|
},
|
||||||
|
exercise: {
|
||||||
|
title: '例如:晨间散步、太极拳',
|
||||||
|
description: '例如:小区公园散步30分钟,注意安全'
|
||||||
|
},
|
||||||
|
meal: {
|
||||||
|
title: '例如:早餐、午餐、晚餐',
|
||||||
|
description: '例如:低盐低糖,营养均衡,多吃蔬菜'
|
||||||
|
},
|
||||||
|
hydration: {
|
||||||
|
title: '例如:饮水提醒',
|
||||||
|
description: '例如:每次200ml左右,温开水为宜'
|
||||||
|
},
|
||||||
|
sleep: {
|
||||||
|
title: '例如:午休、就寝',
|
||||||
|
description: '例如:30-60分钟,保证充足睡眠'
|
||||||
|
},
|
||||||
|
checkup: {
|
||||||
|
title: '例如:血压测量、血糖检测',
|
||||||
|
description: '例如:记录数据并上传,如有异常及时联系医生'
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
title: '例如:阅读时光、听音乐',
|
||||||
|
description: '例如:详细描述此活动的具体内容和注意事项'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return placeholders[type];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColorClasses = (color: string) => {
|
||||||
|
const colors = {
|
||||||
|
blue: 'bg-blue-50 text-blue-700 border-blue-200',
|
||||||
|
green: 'bg-green-50 text-green-700 border-green-200',
|
||||||
|
orange: 'bg-orange-50 text-orange-700 border-orange-200',
|
||||||
|
cyan: 'bg-cyan-50 text-cyan-700 border-cyan-200',
|
||||||
|
purple: 'bg-purple-50 text-purple-700 border-purple-200',
|
||||||
|
red: 'bg-red-50 text-red-700 border-red-200',
|
||||||
|
gray: 'bg-gray-50 text-gray-700 border-gray-200',
|
||||||
|
};
|
||||||
|
return colors[color as keyof typeof colors] || colors.gray;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 始终使用 API 数据
|
||||||
|
let careTasks: CareTask[] = [];
|
||||||
|
try {
|
||||||
|
careTasks = schedules.map(convertScheduleToTask);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('转换日程数据失败:', error);
|
||||||
|
careTasks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredTasks =
|
||||||
|
activeTab === 'all'
|
||||||
|
? careTasks
|
||||||
|
: careTasks.filter((task) => task.type === activeTab);
|
||||||
|
|
||||||
|
const handleEdit = (task: CareTask) => {
|
||||||
|
setEditingTask(task);
|
||||||
|
setFormType(task.type);
|
||||||
|
setFormTitle(task.name);
|
||||||
|
setFormDescription(task.description || '');
|
||||||
|
// 设置时间:从 times 数组中取第一个时间
|
||||||
|
setFormTime(task.times && task.times.length > 0 ? task.times[0] : '08:00');
|
||||||
|
// 设置重复:daily 表示每日重复,once 表示不重复
|
||||||
|
setFormRepeatDaily(task.repeatType === 'daily');
|
||||||
|
// 设置自动提醒:明确使用布尔值,undefined 时默认为 true
|
||||||
|
const autoRemindValue = task.autoRemind === undefined ? true : task.autoRemind;
|
||||||
|
setFormAutoRemind(autoRemindValue);
|
||||||
|
console.log('编辑任务,task.autoRemind:', task.autoRemind, '最终设置为:', autoRemindValue);
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (taskId: string) => {
|
||||||
|
setConfirmDialog({
|
||||||
|
message: '确定要删除这个护理计划吗?删除后无法恢复。',
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
console.log('正在删除日程 ID:', taskId);
|
||||||
|
const success = await scheduleService.deleteSchedule(Number(taskId));
|
||||||
|
console.log('删除结果:', success);
|
||||||
|
|
||||||
|
// 重新加载列表
|
||||||
|
await loadSchedules();
|
||||||
|
console.log('重新加载后的日程数量:', schedules.length);
|
||||||
|
|
||||||
|
setToast({ message: '删除成功', type: 'success' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
setToast({ message: '删除失败,请重试', type: 'error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddNew = (type: CareType) => {
|
||||||
|
setEditingTask(null);
|
||||||
|
setFormType(type);
|
||||||
|
setFormTitle('');
|
||||||
|
setFormDescription('');
|
||||||
|
setFormTime('08:00');
|
||||||
|
setFormRepeatDaily(true);
|
||||||
|
setFormAutoRemind(true);
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
if (loading && schedules.length === 0 && !apiConnected) {
|
||||||
|
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>
|
||||||
|
<button
|
||||||
|
onClick={() => handleAddNew('medication')}
|
||||||
|
className="px-3 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors flex items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<Plus size={18} />
|
||||||
|
添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标签栏 */}
|
||||||
|
<div className="flex gap-2 overflow-x-auto pb-2 -mb-2 scrollbar-hide">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('all')}
|
||||||
|
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
|
||||||
|
activeTab === 'all'
|
||||||
|
? 'bg-primary-100 text-primary-700'
|
||||||
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
全部 ({careTasks.length})
|
||||||
|
</button>
|
||||||
|
{(Object.entries(careTypeConfig) as [CareType, any][]).map(
|
||||||
|
([type, config]) => {
|
||||||
|
const count = careTasks.filter((t) => t.type === type).length;
|
||||||
|
const Icon = config.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
onClick={() => setActiveTab(type)}
|
||||||
|
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors flex items-center gap-1 ${
|
||||||
|
activeTab === type
|
||||||
|
? getColorClasses(config.color)
|
||||||
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon size={16} />
|
||||||
|
{config.label} ({count})
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主要内容区 - 手机优化列表 */}
|
||||||
|
<div className="px-4 py-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{filteredTasks.map((task) => {
|
||||||
|
const config = careTypeConfig[task.type];
|
||||||
|
const Icon = config.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={task.id}
|
||||||
|
className="card p-4 hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||||
|
<div
|
||||||
|
className={`p-2 rounded-lg ${getColorClasses(
|
||||||
|
config.color
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
<Icon size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||||
|
<h3 className="font-bold text-gray-900">
|
||||||
|
{task.name}
|
||||||
|
</h3>
|
||||||
|
{/* 日程状态标签 */}
|
||||||
|
{task.status === 'pending' && (
|
||||||
|
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700">
|
||||||
|
⏳ 待执行
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{task.status === 'completed' && (
|
||||||
|
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">
|
||||||
|
✓ 已完成
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{task.status === 'skipped' && (
|
||||||
|
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
|
||||||
|
○ 已忽略
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{task.status === 'missed' && (
|
||||||
|
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-700">
|
||||||
|
⚠ 已错过
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{/* 如果没有状态,默认显示待执行 */}
|
||||||
|
{!task.status && (
|
||||||
|
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700">
|
||||||
|
⏳ 待执行
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{task.autoRemind && (
|
||||||
|
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-700">
|
||||||
|
🤖 自动提醒
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{task.repeatType === 'daily' && (
|
||||||
|
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700">
|
||||||
|
🔄 每日重复
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 用药专用信息 */}
|
||||||
|
{task.type === 'medication' && (
|
||||||
|
<div className="text-sm text-gray-600 mb-2">
|
||||||
|
{task.dosage} · {task.route}
|
||||||
|
{task.withFood && ' · 随餐'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 其他描述 */}
|
||||||
|
{task.description && (
|
||||||
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
|
{task.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 时间标签 */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{task.times.map((time, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Clock size={12} />
|
||||||
|
{time}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 用药宽限期 */}
|
||||||
|
{task.type === 'medication' && task.gracePeriod && (
|
||||||
|
<div className="text-xs text-gray-500 mt-2">
|
||||||
|
宽限期:{task.gracePeriod} 分钟
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(task)}
|
||||||
|
className="p-2 text-gray-600 hover:text-primary-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
aria-label="编辑"
|
||||||
|
>
|
||||||
|
<Edit2 size={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(task.id)}
|
||||||
|
className="p-2 text-gray-600 hover:text-red-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
aria-label="删除"
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 空状态 */}
|
||||||
|
{filteredTasks.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-gray-500">暂无护理计划</p>
|
||||||
|
<button
|
||||||
|
onClick={() => handleAddNew(activeTab === 'all' ? 'medication' : activeTab)}
|
||||||
|
className="mt-4 text-primary-600 hover:text-primary-700 font-medium"
|
||||||
|
>
|
||||||
|
添加第一个计划
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 提示信息 */}
|
||||||
|
<div className="mt-6 bg-blue-50 border-l-4 border-blue-400 p-4 rounded-lg">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<AlertCircle
|
||||||
|
className="text-blue-600 mt-0.5 mr-3 flex-shrink-0"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
<div className="text-sm text-blue-800">
|
||||||
|
<p className="font-medium mb-1">护理计划提示</p>
|
||||||
|
<ul className="list-disc list-inside space-y-1 text-blue-700">
|
||||||
|
<li>护理计划会在设定时间提醒老人执行</li>
|
||||||
|
<li>用药计划超时未确认将自动发送通知给家属</li>
|
||||||
|
<li>如需调整护理方案请咨询专业医护人员</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 编辑/添加表单弹窗 */}
|
||||||
|
{showForm && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-white rounded-3xl shadow-2xl max-w-md w-full max-h-[85vh] overflow-hidden flex flex-col">
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="p-6 border-b flex items-center justify-between flex-shrink-0">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900">
|
||||||
|
{editingTask ? '编辑' : '添加'}
|
||||||
|
{careTypeConfig[formType]?.label || ''}计划
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowForm(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表单内容 - 可滚动 */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||||
|
{/* 类型选择 */}
|
||||||
|
{!editingTask && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
计划类型
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{(Object.entries(careTypeConfig) as [CareType, any][]).map(
|
||||||
|
([type, config]) => {
|
||||||
|
const Icon = config.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
onClick={() => setFormType(type)}
|
||||||
|
className={`p-3 rounded-lg border-2 transition-colors ${
|
||||||
|
formType === type
|
||||||
|
? `${getColorClasses(config.color)} border-current`
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon size={24} className="mx-auto mb-1" />
|
||||||
|
<div className="text-xs font-medium">
|
||||||
|
{config.label}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 名称 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{formType === 'medication' ? '药品名称' : '计划名称'}{' '}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={getPlaceholders(formType).title}
|
||||||
|
value={formTitle || editingTask?.name || ''}
|
||||||
|
onChange={(e) => setFormTitle(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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 用药专用字段 */}
|
||||||
|
{formType === 'medication' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
剂量 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="例如:50mg"
|
||||||
|
defaultValue={editingTask?.dosage}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
给药途径
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
defaultValue={editingTask?.route}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
>
|
||||||
|
<option>口服</option>
|
||||||
|
<option>外用</option>
|
||||||
|
<option>注射</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked={editingTask?.withFood}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
需要随餐服用
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
宽限期(分钟)
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
defaultValue={editingTask?.gracePeriod}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="15">15 分钟</option>
|
||||||
|
<option value="30">30 分钟</option>
|
||||||
|
<option value="60">60 分钟</option>
|
||||||
|
<option value="120">120 分钟</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 非用药的描述字段 */}
|
||||||
|
{formType !== 'medication' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
描述说明
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
placeholder={getPlaceholders(formType).description}
|
||||||
|
value={formDescription || editingTask?.description || ''}
|
||||||
|
onChange={(e) => setFormDescription(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 执行时间 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
执行时间 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={formTime}
|
||||||
|
onChange={(e) => setFormTime(e.target.value)}
|
||||||
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 是否每日重复 */}
|
||||||
|
<div>
|
||||||
|
<label className="flex items-start gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formRepeatDaily}
|
||||||
|
onChange={(e) => setFormRepeatDaily(e.target.checked)}
|
||||||
|
className="mt-1 rounded"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="text-sm font-medium text-gray-700">
|
||||||
|
每日重复执行
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
启用后,此日程将每天在设定的时间自动执行
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 数字人自动提醒 */}
|
||||||
|
<div className="pt-4 border-t">
|
||||||
|
<label className="flex items-start gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formAutoRemind}
|
||||||
|
onChange={(e) => {
|
||||||
|
console.log('自动提醒复选框改变:', e.target.checked);
|
||||||
|
setFormAutoRemind(e.target.checked);
|
||||||
|
}}
|
||||||
|
className="mt-1 rounded"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="text-sm font-medium text-gray-700">
|
||||||
|
数字人自动提醒老人 {formAutoRemind ? '(当前: 是)' : '(当前: 否)'}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
启用后,数字人会在设定时间主动提醒老人执行此护理任务
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部按钮 */}
|
||||||
|
<div className="p-4 border-t flex gap-3 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowForm(false)}
|
||||||
|
className="flex-1 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
// 验证必填字段
|
||||||
|
const title = formTitle.trim() || editingTask?.name || '';
|
||||||
|
if (!title) {
|
||||||
|
setToast({ message: '请输入日程名称', type: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建日程时间:使用今天的日期 + 用户选择的时间
|
||||||
|
const today = new Date();
|
||||||
|
const [hours, minutes] = formTime.split(':').map(Number);
|
||||||
|
const scheduleDateTime = new Date(
|
||||||
|
today.getFullYear(),
|
||||||
|
today.getMonth(),
|
||||||
|
today.getDate(),
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取表单数据
|
||||||
|
const formData: scheduleService.Schedule = {
|
||||||
|
family_id: familyId,
|
||||||
|
title: title,
|
||||||
|
description: formDescription.trim() || editingTask?.description || '',
|
||||||
|
schedule_type: formType === 'hydration' || formType === 'sleep' ? 'other' : formType,
|
||||||
|
schedule_time: scheduleService.formatDateTime(scheduleDateTime),
|
||||||
|
repeat_type: formRepeatDaily ? 'daily' : 'once',
|
||||||
|
auto_remind: formAutoRemind ? 1 : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果是编辑操作,且新时间在当前时间之后,自动重置状态为pending
|
||||||
|
if (editingTask) {
|
||||||
|
const now = new Date();
|
||||||
|
if (scheduleDateTime > now) {
|
||||||
|
formData.status = 'pending';
|
||||||
|
console.log('日程时间在未来,自动重置状态为pending');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingTask) {
|
||||||
|
// 更新
|
||||||
|
await scheduleService.updateSchedule(Number(editingTask.id), formData);
|
||||||
|
console.log('更新成功');
|
||||||
|
} else {
|
||||||
|
// 创建
|
||||||
|
const newId = await scheduleService.createSchedule(formData);
|
||||||
|
console.log('创建成功,新 ID:', newId);
|
||||||
|
}
|
||||||
|
setShowForm(false);
|
||||||
|
setFormTitle('');
|
||||||
|
setFormDescription('');
|
||||||
|
setFormTime('08:00');
|
||||||
|
setFormRepeatDaily(true);
|
||||||
|
setFormAutoRemind(true);
|
||||||
|
// 等待重新加载完成
|
||||||
|
await loadSchedules();
|
||||||
|
setToast({
|
||||||
|
message: editingTask ? '更新成功' : '创建成功',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败:', error);
|
||||||
|
setToast({
|
||||||
|
message: `保存失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex-1 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Toast 提示 */}
|
||||||
|
{toast && (
|
||||||
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
onClose={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 确认对话框 */}
|
||||||
|
{confirmDialog && (
|
||||||
|
<ConfirmDialog
|
||||||
|
message={confirmDialog.message}
|
||||||
|
type="danger"
|
||||||
|
confirmText="删除"
|
||||||
|
onConfirm={confirmDialog.onConfirm}
|
||||||
|
onCancel={() => setConfirmDialog(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user