Add File
This commit is contained in:
470
src/family/components/ElderProfile.tsx
Normal file
470
src/family/components/ElderProfile.tsx
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
X,
|
||||||
|
User,
|
||||||
|
Calendar,
|
||||||
|
Brain,
|
||||||
|
Eye,
|
||||||
|
Ear,
|
||||||
|
Music,
|
||||||
|
AlertCircle,
|
||||||
|
Heart,
|
||||||
|
Edit3,
|
||||||
|
Save,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 老人个人信息弹窗(带编辑功能)
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ElderInfo {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
cognitive_status: 'normal' | 'mild' | 'moderate' | 'severe';
|
||||||
|
hearing_vision: {
|
||||||
|
hearing: 'ok' | 'mild_loss' | 'moderate_loss' | 'severe_loss';
|
||||||
|
vision: 'ok' | 'mild_loss' | 'moderate_loss' | 'severe_loss';
|
||||||
|
};
|
||||||
|
preferences: {
|
||||||
|
music: string[];
|
||||||
|
avoid_topics: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElderProfileProps {
|
||||||
|
elderInfo: ElderInfo;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave?: (updatedInfo: ElderInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ElderProfile: React.FC<ElderProfileProps> = ({
|
||||||
|
elderInfo,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
}) => {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editedInfo, setEditedInfo] = useState<ElderInfo>(elderInfo);
|
||||||
|
const [newInterest, setNewInterest] = useState('');
|
||||||
|
const [newAvoidTopic, setNewAvoidTopic] = useState('');
|
||||||
|
|
||||||
|
const getCognitiveStatusLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'normal':
|
||||||
|
return '正常';
|
||||||
|
case 'mild':
|
||||||
|
return '轻度认知障碍';
|
||||||
|
case 'moderate':
|
||||||
|
return '中度认知障碍';
|
||||||
|
case 'severe':
|
||||||
|
return '重度认知障碍';
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCognitiveStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'normal':
|
||||||
|
return 'text-green-600 bg-green-50 border-green-200';
|
||||||
|
case 'mild':
|
||||||
|
return 'text-yellow-600 bg-yellow-50 border-yellow-200';
|
||||||
|
case 'moderate':
|
||||||
|
return 'text-orange-600 bg-orange-50 border-orange-200';
|
||||||
|
case 'severe':
|
||||||
|
return 'text-red-600 bg-red-50 border-red-200';
|
||||||
|
default:
|
||||||
|
return 'text-gray-600 bg-gray-50 border-gray-200';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHealthStatusLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'ok':
|
||||||
|
return '正常';
|
||||||
|
case 'mild_loss':
|
||||||
|
return '轻度下降';
|
||||||
|
case 'moderate_loss':
|
||||||
|
return '中度下降';
|
||||||
|
case 'severe_loss':
|
||||||
|
return '重度下降';
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHealthStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'ok':
|
||||||
|
return 'text-green-600';
|
||||||
|
case 'mild_loss':
|
||||||
|
return 'text-yellow-600';
|
||||||
|
case 'moderate_loss':
|
||||||
|
return 'text-orange-600';
|
||||||
|
case 'severe_loss':
|
||||||
|
return 'text-red-600';
|
||||||
|
default:
|
||||||
|
return 'text-gray-600';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (onSave) {
|
||||||
|
onSave(editedInfo);
|
||||||
|
}
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditedInfo(elderInfo);
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addInterest = () => {
|
||||||
|
if (newInterest.trim()) {
|
||||||
|
setEditedInfo({
|
||||||
|
...editedInfo,
|
||||||
|
preferences: {
|
||||||
|
...editedInfo.preferences,
|
||||||
|
music: [...editedInfo.preferences.music, newInterest.trim()],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setNewInterest('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeInterest = (index: number) => {
|
||||||
|
setEditedInfo({
|
||||||
|
...editedInfo,
|
||||||
|
preferences: {
|
||||||
|
...editedInfo.preferences,
|
||||||
|
music: editedInfo.preferences.music.filter((_, i) => i !== index),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAvoidTopic = () => {
|
||||||
|
if (newAvoidTopic.trim()) {
|
||||||
|
setEditedInfo({
|
||||||
|
...editedInfo,
|
||||||
|
preferences: {
|
||||||
|
...editedInfo.preferences,
|
||||||
|
avoid_topics: [...editedInfo.preferences.avoid_topics, newAvoidTopic.trim()],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setNewAvoidTopic('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAvoidTopic = (index: number) => {
|
||||||
|
setEditedInfo({
|
||||||
|
...editedInfo,
|
||||||
|
preferences: {
|
||||||
|
...editedInfo.preferences,
|
||||||
|
avoid_topics: editedInfo.preferences.avoid_topics.filter((_, i) => i !== index),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentInfo = isEditing ? editedInfo : elderInfo;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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="bg-gradient-to-br from-primary-500 to-primary-600 p-6 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4 flex-1 min-w-0">
|
||||||
|
<div className="w-16 h-16 bg-white rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
|
<User size={32} className="text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<div className="text-white flex-1 min-w-0">
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedInfo.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditedInfo({ ...editedInfo, name: e.target.value })
|
||||||
|
}
|
||||||
|
className="text-2xl font-bold bg-white/20 rounded-lg px-3 py-1 w-full text-white placeholder-white/70"
|
||||||
|
placeholder="姓名"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<h2 className="text-2xl font-bold">{currentInfo.name}</h2>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<Calendar size={16} className="flex-shrink-0" />
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editedInfo.age}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditedInfo({ ...editedInfo, age: parseInt(e.target.value) || 0 })
|
||||||
|
}
|
||||||
|
className="bg-white/20 rounded px-2 py-0.5 w-16 text-primary-100"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-primary-100">{currentInfo.age} 岁</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-white hover:bg-white/20 rounded-full p-2 transition-colors flex-shrink-0"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容区 - 可滚动 */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||||
|
{/* 认知状态 */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Brain size={20} className="text-primary-600" />
|
||||||
|
<h3 className="text-base font-bold text-gray-900">认知状态</h3>
|
||||||
|
</div>
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{(['normal', 'mild', 'moderate', 'severe'] as const).map((status) => (
|
||||||
|
<button
|
||||||
|
key={status}
|
||||||
|
onClick={() =>
|
||||||
|
setEditedInfo({ ...editedInfo, cognitive_status: status })
|
||||||
|
}
|
||||||
|
className={`px-4 py-2 rounded-full text-sm font-medium border-2 transition-colors ${
|
||||||
|
editedInfo.cognitive_status === status
|
||||||
|
? getCognitiveStatusColor(status)
|
||||||
|
: 'bg-gray-50 text-gray-600 border-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getCognitiveStatusLabel(status)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-full ${getCognitiveStatusColor(
|
||||||
|
currentInfo.cognitive_status
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
<span className="font-medium">
|
||||||
|
{getCognitiveStatusLabel(currentInfo.cognitive_status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 听力与视力 */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Heart size={20} className="text-primary-600" />
|
||||||
|
<h3 className="text-base font-bold text-gray-900">健康状况</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 听力 */}
|
||||||
|
<div className="p-3 bg-gray-50 rounded-xl">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Ear size={18} className="text-gray-600" />
|
||||||
|
<span className="text-sm font-medium text-gray-700">听力</span>
|
||||||
|
</div>
|
||||||
|
{isEditing ? (
|
||||||
|
<select
|
||||||
|
value={editedInfo.hearing_vision.hearing}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditedInfo({
|
||||||
|
...editedInfo,
|
||||||
|
hearing_vision: {
|
||||||
|
...editedInfo.hearing_vision,
|
||||||
|
hearing: e.target.value as any,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
<option value="ok">正常</option>
|
||||||
|
<option value="mild_loss">轻度下降</option>
|
||||||
|
<option value="moderate_loss">中度下降</option>
|
||||||
|
<option value="severe_loss">重度下降</option>
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium ${getHealthStatusColor(
|
||||||
|
currentInfo.hearing_vision.hearing
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{getHealthStatusLabel(currentInfo.hearing_vision.hearing)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 视力 */}
|
||||||
|
<div className="p-3 bg-gray-50 rounded-xl">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Eye size={18} className="text-gray-600" />
|
||||||
|
<span className="text-sm font-medium text-gray-700">视力</span>
|
||||||
|
</div>
|
||||||
|
{isEditing ? (
|
||||||
|
<select
|
||||||
|
value={editedInfo.hearing_vision.vision}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditedInfo({
|
||||||
|
...editedInfo,
|
||||||
|
hearing_vision: {
|
||||||
|
...editedInfo.hearing_vision,
|
||||||
|
vision: e.target.value as any,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
<option value="ok">正常</option>
|
||||||
|
<option value="mild_loss">轻度下降</option>
|
||||||
|
<option value="moderate_loss">中度下降</option>
|
||||||
|
<option value="severe_loss">重度下降</option>
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium ${getHealthStatusColor(
|
||||||
|
currentInfo.hearing_vision.vision
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{getHealthStatusLabel(currentInfo.hearing_vision.vision)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 兴趣偏好 */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Music size={20} className="text-primary-600" />
|
||||||
|
<h3 className="text-base font-bold text-gray-900">兴趣偏好</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{currentInfo.preferences.music.map((item, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="px-3 py-1.5 bg-blue-50 text-blue-700 rounded-full text-sm font-medium flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
{isEditing && (
|
||||||
|
<button
|
||||||
|
onClick={() => removeInterest(index)}
|
||||||
|
className="ml-1 hover:bg-blue-100 rounded-full p-0.5"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{isEditing && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newInterest}
|
||||||
|
onChange={(e) => setNewInterest(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && addInterest()}
|
||||||
|
placeholder="添加兴趣(如:唱歌、下棋)"
|
||||||
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={addInterest}
|
||||||
|
className="px-3 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 避免话题 */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<AlertCircle size={20} className="text-primary-600" />
|
||||||
|
<h3 className="text-base font-bold text-gray-900">避免话题</h3>
|
||||||
|
</div>
|
||||||
|
{currentInfo.preferences.avoid_topics.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{currentInfo.preferences.avoid_topics.map((item, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="px-3 py-1.5 bg-red-50 text-red-700 rounded-full text-sm font-medium flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
{isEditing && (
|
||||||
|
<button
|
||||||
|
onClick={() => removeAvoidTopic(index)}
|
||||||
|
className="ml-1 hover:bg-red-100 rounded-full p-0.5"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isEditing && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newAvoidTopic}
|
||||||
|
onChange={(e) => setNewAvoidTopic(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && addAvoidTopic()}
|
||||||
|
placeholder="添加避免话题"
|
||||||
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={addAvoidTopic}
|
||||||
|
className="px-3 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部按钮 */}
|
||||||
|
<div className="p-4 border-t">
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="flex-1 py-3 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="flex-1 py-3 bg-primary-500 hover:bg-primary-600 text-white font-medium rounded-xl transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Save size={20} />
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
className="flex-1 py-3 bg-primary-500 hover:bg-primary-600 text-white font-medium rounded-xl transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Edit3 size={20} />
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 py-3 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user