Add File
This commit is contained in:
231
src/family/pages/MoodHistory.tsx
Normal file
231
src/family/pages/MoodHistory.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Calendar,
|
||||
TrendingUp,
|
||||
ChevronDown,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import * as moodService from '../services/moodService';
|
||||
|
||||
/**
|
||||
* 情绪记录历史页面
|
||||
* 展示老人的情绪记录和统计数据
|
||||
*/
|
||||
export const MoodHistory: React.FC = () => {
|
||||
const [records, setRecords] = useState<moodService.MoodRecord[]>([]);
|
||||
const [stats, setStats] = useState<moodService.MoodStatsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedDays, setSelectedDays] = useState(7);
|
||||
const [showDaysDropdown, setShowDaysDropdown] = useState(false);
|
||||
const familyId = 'family_001';
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [recordsData, statsData] = await Promise.all([
|
||||
moodService.getFamilyMoods(familyId, { limit: 50 }),
|
||||
moodService.getMoodStats(familyId, { days: selectedDays }),
|
||||
]);
|
||||
setRecords(recordsData.records);
|
||||
setStats(statsData);
|
||||
} catch (error) {
|
||||
console.error('加载情绪数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [selectedDays]);
|
||||
|
||||
const daysOptions = [
|
||||
{ value: 7, label: '最近7天' },
|
||||
{ value: 14, label: '最近14天' },
|
||||
{ value: 30, label: '最近30天' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
{/* 页面标题和刷新 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-bold text-gray-900">情绪记录</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 时间范围选择 */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowDaysDropdown(!showDaysDropdown)}
|
||||
className="flex items-center gap-1 px-3 py-1.5 bg-gray-100 rounded-lg text-sm"
|
||||
>
|
||||
<Calendar size={14} />
|
||||
{daysOptions.find(o => o.value === selectedDays)?.label}
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
{showDaysDropdown && (
|
||||
<div className="absolute right-0 mt-1 bg-white border rounded-lg shadow-lg z-10">
|
||||
{daysOptions.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
setSelectedDays(option.value);
|
||||
setShowDaysDropdown(false);
|
||||
}}
|
||||
className={`block w-full text-left px-4 py-2 text-sm hover:bg-gray-50 ${
|
||||
selectedDays === option.value ? 'bg-primary-50 text-primary-600' : ''
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="p-1.5 bg-gray-100 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<RefreshCw className="animate-spin text-gray-400" size={24} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 统计卡片 */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* 整体统计 */}
|
||||
<div className="bg-white rounded-xl p-4 border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingUp size={16} className="text-primary-500" />
|
||||
<span className="text-sm font-medium text-gray-600">平均情绪</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span
|
||||
className="text-2xl font-bold"
|
||||
style={{ color: moodService.getMoodScoreColor(stats.overall.avg_score) }}
|
||||
>
|
||||
{stats.overall.avg_score}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">/ 10</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{moodService.formatMoodScore(stats.overall.avg_score)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 今日记录 */}
|
||||
<div className="bg-white rounded-xl p-4 border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Calendar size={16} className="text-blue-500" />
|
||||
<span className="text-sm font-medium text-gray-600">今日记录</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
{stats.today_count}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">次</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
共 {stats.overall.total_records} 条记录
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 情绪类型分布 */}
|
||||
{stats && stats.mood_type_stats.length > 0 && (
|
||||
<div className="bg-white rounded-xl p-4 border">
|
||||
<h3 className="text-sm font-medium text-gray-600 mb-3">情绪分布</h3>
|
||||
<div className="space-y-2">
|
||||
{stats.mood_type_stats.map(stat => (
|
||||
<div key={stat.mood_type} className="flex items-center gap-3">
|
||||
<span className="text-lg">
|
||||
{moodService.moodEmojiMap[stat.mood_type]}
|
||||
</span>
|
||||
<span className="text-sm text-gray-700 w-12">
|
||||
{moodService.moodLabelMap[stat.mood_type]}
|
||||
</span>
|
||||
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full"
|
||||
style={{
|
||||
width: `${(stat.count / stats.overall.total_records) * 100}%`,
|
||||
backgroundColor: moodService.moodColorMap[stat.mood_type],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 w-8 text-right">
|
||||
{stat.count}次
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 历史记录列表 */}
|
||||
<div className="bg-white rounded-xl border">
|
||||
<div className="p-4 border-b">
|
||||
<h3 className="text-sm font-medium text-gray-600">历史记录</h3>
|
||||
</div>
|
||||
{records.length === 0 ? (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<p>暂无情绪记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y max-h-96 overflow-y-auto">
|
||||
{records.map(record => (
|
||||
<div key={record.id} className="p-4 flex items-center gap-3">
|
||||
<span className="text-2xl">
|
||||
{moodService.moodEmojiMap[record.mood_type]}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">
|
||||
{moodService.moodLabelMap[record.mood_type]}
|
||||
</span>
|
||||
<span
|
||||
className="px-1.5 py-0.5 rounded text-xs font-medium"
|
||||
style={{
|
||||
backgroundColor: `${moodService.getMoodScoreColor(record.mood_score)}20`,
|
||||
color: moodService.getMoodScoreColor(record.mood_score),
|
||||
}}
|
||||
>
|
||||
{record.mood_score}分
|
||||
</span>
|
||||
</div>
|
||||
{record.note && (
|
||||
<p className="text-sm text-gray-500 truncate mt-0.5">
|
||||
{record.note}
|
||||
</p>
|
||||
)}
|
||||
{record.trigger_event && (
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
触发: {record.trigger_event}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500">
|
||||
{record.recorded_at && moodService.formatRecordTime(record.recorded_at)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{record.source === 'manual' ? '手动' : record.source === 'ai_detect' ? 'AI' : '语音'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user