Add File
This commit is contained in:
284
src/family/pages/Dashboard.tsx
Normal file
284
src/family/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
MessageCircle,
|
||||||
|
Heart,
|
||||||
|
Image as ImageIcon,
|
||||||
|
AlertTriangle,
|
||||||
|
Video,
|
||||||
|
Maximize2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { MetricCard } from '../components/MetricCard';
|
||||||
|
import { TrendChart } from '../components/TrendChart';
|
||||||
|
import * as moodService from '../services/moodService';
|
||||||
|
import * as mediaService from '../services/mediaService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 家属端 Dashboard - 今天概览
|
||||||
|
* 手机浏览器优化 - 单列布局,紧凑显示
|
||||||
|
* 一页看完今天所有重要信息
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface DashboardProps {
|
||||||
|
onNavigate?: (page: 'interaction' | 'messages' | 'care' | 'alerts' | 'media' | 'mood') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
username: string;
|
||||||
|
is_adopted: number;
|
||||||
|
type: 'fay' | 'member';
|
||||||
|
way: string;
|
||||||
|
content: string;
|
||||||
|
createtime: number;
|
||||||
|
timetext: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_BASE_URL = 'http://127.0.0.1:5000';
|
||||||
|
const familyId = 'family_001';
|
||||||
|
|
||||||
|
export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
||||||
|
const [isVideoFullscreen, setIsVideoFullscreen] = useState(false);
|
||||||
|
const [todayInteractionCount, setTodayInteractionCount] = useState(0);
|
||||||
|
const [latestMood, setLatestMood] = useState<moodService.MoodRecord | null>(null);
|
||||||
|
const [moodStats, setMoodStats] = useState<moodService.MoodStatsResponse | null>(null);
|
||||||
|
const [recentPlays, setRecentPlays] = useState<mediaService.RecentPlay[]>([]);
|
||||||
|
|
||||||
|
// 加载今日交互次数
|
||||||
|
const loadTodayInteractionCount = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/get-msg`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username: 'User', limit: 300 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const messages: ChatMessage[] = data.list || [];
|
||||||
|
|
||||||
|
// 获取今天的开始时间戳(00:00:00)
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const todayTimestamp = Math.floor(today.getTime() / 1000);
|
||||||
|
|
||||||
|
// 统计今天老人说的话(type === 'member')
|
||||||
|
const todayMemberMessages = messages.filter(
|
||||||
|
msg => msg.type === 'member' && msg.createtime >= todayTimestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
setTodayInteractionCount(todayMemberMessages.length);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取今日交互次数失败:', error);
|
||||||
|
// 失败时保持为0,不影响页面显示
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载情绪数据
|
||||||
|
const loadMoodData = async () => {
|
||||||
|
try {
|
||||||
|
const [latest, stats] = await Promise.all([
|
||||||
|
moodService.getFamilyMoods(familyId, { limit: 1 }),
|
||||||
|
moodService.getMoodStats(familyId, { days: 7 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (latest.records.length > 0) {
|
||||||
|
setLatestMood(latest.records[0]);
|
||||||
|
}
|
||||||
|
setMoodStats(stats);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载情绪数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载最近播放
|
||||||
|
const loadRecentPlays = async () => {
|
||||||
|
try {
|
||||||
|
const plays = await mediaService.getRecentPlays(familyId, 2);
|
||||||
|
setRecentPlays(plays);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载最近播放失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTodayInteractionCount();
|
||||||
|
loadMoodData();
|
||||||
|
loadRecentPlays();
|
||||||
|
// 每30秒刷新一次
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
loadTodayInteractionCount();
|
||||||
|
loadMoodData();
|
||||||
|
loadRecentPlays();
|
||||||
|
}, 30000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 情绪趋势数据 - 使用真实数据或默认数据
|
||||||
|
const emotionData = moodStats?.daily_stats.map(stat => ({
|
||||||
|
date: new Date(stat.date).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' }),
|
||||||
|
value: stat.avg_score,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
|
||||||
|
// 格式化播放时间
|
||||||
|
const formatPlayTime = (playedAt: string) => {
|
||||||
|
const playTime = new Date(playedAt);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - playTime.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
const diffHours = Math.floor(diffMs / 3600000);
|
||||||
|
const diffDays = Math.floor(diffMs / 86400000);
|
||||||
|
|
||||||
|
if (diffMins < 1) return '刚刚';
|
||||||
|
if (diffMins < 60) return `${diffMins}分钟前`;
|
||||||
|
if (diffHours < 24) return `${diffHours}小时前`;
|
||||||
|
if (diffDays === 1) return '昨天';
|
||||||
|
if (diffDays < 7) return `${diffDays}天前`;
|
||||||
|
|
||||||
|
return playTime.toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* 主要内容区 - 手机优化 */}
|
||||||
|
<div className="px-4 py-4">
|
||||||
|
{/* 实时监控视频 */}
|
||||||
|
<div className="card p-0 mb-4 overflow-hidden">
|
||||||
|
<div className="relative">
|
||||||
|
{/* 视频播放器 */}
|
||||||
|
<div className="relative bg-gray-900 aspect-video">
|
||||||
|
{/* 模拟视频画面 */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<Video size={48} className="text-gray-600" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 实时状态标签 */}
|
||||||
|
<div className="absolute top-3 left-3 flex items-center gap-1.5 bg-red-600 text-white px-2.5 py-1 rounded-full text-xs font-medium">
|
||||||
|
<div className="w-2 h-2 bg-white rounded-full animate-pulse"></div>
|
||||||
|
实时
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 全屏按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsVideoFullscreen(!isVideoFullscreen)}
|
||||||
|
className="absolute top-3 right-3 bg-black/50 hover:bg-black/70 text-white p-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Maximize2 size={18} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 视频信息叠加 */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3">
|
||||||
|
<div className="text-white">
|
||||||
|
<div className="text-xs opacity-90 mb-0.5">客厅监控</div>
|
||||||
|
<div className="text-sm font-medium">张奶奶正在与数字人对话</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 视频控制栏 */}
|
||||||
|
<div className="bg-white border-t border-gray-200 px-4 py-2.5">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-gray-600">画质:高清</span>
|
||||||
|
<span className="text-gray-300">|</span>
|
||||||
|
<span className="text-gray-600">延迟:<1s</span>
|
||||||
|
</div>
|
||||||
|
<button className="text-primary-600 hover:text-primary-700 font-medium">
|
||||||
|
切换摄像头
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* 关键指标卡片 - 手机单列布局 */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||||
|
<MetricCard
|
||||||
|
title="今日交互"
|
||||||
|
value={`${todayInteractionCount}次`}
|
||||||
|
subtitle="老人与数字人对话"
|
||||||
|
icon={MessageCircle}
|
||||||
|
color="blue"
|
||||||
|
onClick={() => onNavigate?.('interaction')}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="情绪状态"
|
||||||
|
value={latestMood ? moodService.moodEmojiMap[latestMood.mood_type] : '暂无记录'}
|
||||||
|
subtitle={latestMood
|
||||||
|
? `${latestMood.mood_score}分 · ${moodService.formatRecordTime(latestMood.recorded_at || latestMood.created_at || '')}`
|
||||||
|
: '等待老人记录情绪'}
|
||||||
|
icon={Heart}
|
||||||
|
color={latestMood && latestMood.mood_score >= 6 ? 'green' : latestMood && latestMood.mood_score >= 4 ? 'yellow' : 'red'}
|
||||||
|
onClick={() => onNavigate?.('mood')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 趋势图表 - 手机单列堆叠 */}
|
||||||
|
<div className="space-y-4 mb-4">
|
||||||
|
<TrendChart
|
||||||
|
title="近 7 天情绪趋势"
|
||||||
|
data={emotionData}
|
||||||
|
color="#10b981"
|
||||||
|
height={220}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 最近播放 - 手机优化 */}
|
||||||
|
<div className="card p-4 mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="text-base font-bold text-gray-900">最近播放</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate?.('media')}
|
||||||
|
className="text-sm text-primary-600 hover:text-primary-700 font-medium"
|
||||||
|
>
|
||||||
|
全部
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{recentPlays.length > 0 ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{recentPlays.map((media) => (
|
||||||
|
<div
|
||||||
|
key={media.id}
|
||||||
|
className="flex gap-3 p-3 rounded-lg border border-gray-200 active:bg-primary-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 bg-gray-200 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||||
|
{media.thumbnail_path ? (
|
||||||
|
<img
|
||||||
|
src={mediaService.getThumbnailUrl(media.thumbnail_path)}
|
||||||
|
alt={media.title}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ImageIcon size={24} className="text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 mb-1 truncate">
|
||||||
|
{media.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-gray-500 mb-2">{formatPlayTime(media.played_at)}</p>
|
||||||
|
<div className="flex items-center gap-3 text-xs">
|
||||||
|
<span className="text-green-600">👍 {media.likes}</span>
|
||||||
|
<span className="text-red-600">👎 {media.dislikes}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
<p className="text-sm">还没有播放记录</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user