This commit is contained in:
2025-12-13 14:46:12 +08:00
parent e443a4d19b
commit a35fff437c

View File

@@ -0,0 +1,300 @@
import React, { useState, useEffect } from 'react';
import { ThumbsUp, ThumbsDown, X, Grid3x3 } from 'lucide-react';
import * as mediaService from '../services/mediaService';
import { MediaLibraryGrid } from './MediaLibraryGrid';
interface MediaPlayerProps {
familyId: string;
elderlyId: number;
currentMood?: string;
onClose?: () => void;
}
/**
* 老人端媒体播放器组件
* 自动获取推荐媒体并播放,支持点赞/点踩反馈
*/
export const MediaPlayer: React.FC<MediaPlayerProps> = ({
familyId,
elderlyId,
currentMood,
onClose,
}) => {
const [recommendedMedia, setRecommendedMedia] = useState<mediaService.RecommendedMedia[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [loading, setLoading] = useState(true);
const [playStartTime, setPlayStartTime] = useState<Date | null>(null);
const [hasGivenFeedback, setHasGivenFeedback] = useState(false);
const [showGrid, setShowGrid] = useState(false);
// 加载推荐媒体
useEffect(() => {
loadRecommendedMedia();
}, [familyId, elderlyId, currentMood]);
// 记录播放开始时间
useEffect(() => {
if (currentMedia) {
setPlayStartTime(new Date());
setHasGivenFeedback(false);
}
}, [currentIndex]);
const loadRecommendedMedia = async () => {
try {
setLoading(true);
const media = await mediaService.getRecommendedMedia(
familyId,
elderlyId,
currentMood
);
setRecommendedMedia(media);
// 如果有媒体,记录第一个的播放
if (media.length > 0) {
recordPlayStart(media[0].id);
}
} catch (error) {
console.error('加载推荐媒体失败:', error);
} finally {
setLoading(false);
}
};
// 记录播放开始(不等待完成)
const recordPlayStart = async (mediaId: number) => {
try {
await mediaService.recordMediaPlay(mediaId, {
elderly_id: elderlyId,
duration_watched: 0,
completed: 0,
triggered_by: 'auto',
mood_before: currentMood,
});
console.log('播放记录已创建, mediaId:', mediaId);
} catch (error) {
console.error('记录播放开始失败:', error);
}
};
const currentMedia = recommendedMedia[currentIndex];
// 记录播放
const recordPlay = async (completed: boolean = false) => {
if (!currentMedia || !playStartTime) return;
const durationWatched = Math.floor((new Date().getTime() - playStartTime.getTime()) / 1000);
try {
await mediaService.recordMediaPlay(currentMedia.id, {
elderly_id: elderlyId,
duration_watched: durationWatched,
completed: completed ? 1 : 0,
triggered_by: 'auto',
mood_before: currentMood,
});
} catch (error) {
console.error('记录播放失败:', error);
}
};
// 处理反馈
const handleFeedback = async (type: 'like' | 'dislike') => {
if (!currentMedia || hasGivenFeedback) return;
try {
await mediaService.submitFeedback(currentMedia.id, {
elderly_id: elderlyId,
feedback_type: type,
});
setHasGivenFeedback(true);
// 自动切换到下一个
setTimeout(() => {
handleNext();
}, 1000);
} catch (error) {
console.error('提交反馈失败:', error);
}
};
// 下一个媒体
const handleNext = async () => {
await recordPlay(true);
if (currentIndex < recommendedMedia.length - 1) {
const nextIndex = currentIndex + 1;
setCurrentIndex(nextIndex);
// 记录下一个媒体的播放
recordPlayStart(recommendedMedia[nextIndex].id);
} else {
// 已经是最后一个,重新加载
await loadRecommendedMedia();
setCurrentIndex(0);
}
};
// 关闭播放器
const handleClose = async () => {
await recordPlay(false);
onClose?.();
};
if (loading) {
return (
<div className="fixed inset-0 bg-black flex items-center justify-center z-50">
<div className="text-white text-xl">...</div>
</div>
);
}
if (!currentMedia) {
return (
<div className="fixed inset-0 bg-black flex items-center justify-center z-50">
<div className="text-white text-center">
<p className="text-xl mb-4"></p>
<button
onClick={handleClose}
className="px-6 py-3 bg-white text-black rounded-lg hover:bg-gray-200 transition-colors"
>
</button>
</div>
</div>
);
}
return (
<div className="fixed inset-0 bg-black flex flex-col z-50">
{/* 数字人小圆圈头像 - 左上角 */}
<div className="absolute top-6 left-6 z-20">
<div className="relative">
{/* 数字人圆圈 */}
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-primary-400 to-primary-500 shadow-2xl flex items-center justify-center border-4 border-white/30 backdrop-blur-sm animate-breathe">
<div className="text-5xl">👤</div>
</div>
{/* 提示文字 */}
<div className="absolute -bottom-10 left-0 bg-white/90 backdrop-blur-sm px-3 py-1.5 rounded-full shadow-lg whitespace-nowrap">
<p className="text-xs font-medium text-gray-700"></p>
</div>
</div>
</div>
{/* 标题和进度 - 顶部中间 */}
<div className="absolute top-6 left-0 right-0 z-10 flex flex-col items-center gap-2 px-6">
<p className="text-2xl font-bold text-white bg-black/60 backdrop-blur-sm px-6 py-3 rounded-2xl">
{currentMedia.title}
</p>
{recommendedMedia.length > 1 && (
<p className="text-base text-white bg-black/40 backdrop-blur-sm px-4 py-2 rounded-full">
{currentIndex + 1} / {recommendedMedia.length}
</p>
)}
</div>
{/* 媒体内容区 */}
<div className="flex-1 flex items-center justify-center p-8">
{currentMedia.media_type === 'photo' ? (
<img
src={mediaService.getMediaUrl(currentMedia.file_path)}
alt={currentMedia.title}
className="max-w-full max-h-full object-contain"
/>
) : (
<video
src={mediaService.getMediaUrl(currentMedia.file_path)}
controls
autoPlay
className="max-w-full max-h-full"
onEnded={handleNext}
/>
)}
</div>
{/* 底部操作栏 - 悬浮 */}
<div className="absolute bottom-0 left-0 right-0 z-10 bg-gradient-to-t from-black/70 to-transparent backdrop-blur-md p-4">
<div className="flex items-center justify-center gap-4 max-w-lg mx-auto">
{/* 喜欢按钮 */}
<button
onClick={() => handleFeedback('like')}
disabled={hasGivenFeedback}
className={`
${hasGivenFeedback ? 'bg-gray-600 cursor-not-allowed' : 'bg-green-500 hover:bg-green-600'}
text-white
flex items-center justify-center gap-2
px-6 py-4
rounded-2xl
shadow-xl
active:scale-95
transition-all
flex-1
`}
>
<ThumbsUp size={28} strokeWidth={2.5} />
<span className="text-lg font-bold"></span>
</button>
{/* 不喜欢按钮 */}
<button
onClick={() => handleFeedback('dislike')}
disabled={hasGivenFeedback}
className={`
${hasGivenFeedback ? 'bg-gray-600 cursor-not-allowed' : 'bg-red-500 hover:bg-red-600'}
text-white
flex items-center justify-center gap-2
px-6 py-4
rounded-2xl
shadow-xl
active:scale-95
transition-all
flex-1
`}
>
<ThumbsDown size={28} strokeWidth={2.5} />
<span className="text-lg font-bold"></span>
</button>
{/* 显示全部按钮 */}
{recommendedMedia.length > 1 && (
<button
onClick={() => setShowGrid(true)}
className="bg-blue-600 hover:bg-blue-700 text-white p-4 rounded-2xl shadow-xl active:scale-95 transition-all"
aria-label="查看全部"
>
<Grid3x3 size={28} strokeWidth={2.5} />
</button>
)}
{/* 关闭按钮 */}
<button
onClick={handleClose}
className="bg-gray-700 hover:bg-gray-800 text-white p-4 rounded-2xl shadow-xl active:scale-95 transition-all"
aria-label="关闭"
>
<X size={28} strokeWidth={2.5} />
</button>
</div>
</div>
{/* 网格视图弹窗 */}
{showGrid && (
<MediaLibraryGrid
mediaList={recommendedMedia.map(m => ({
id: m.id.toString(),
url: mediaService.getMediaUrl(m.file_path),
thumbnailUrl: m.thumbnail_path ? mediaService.getThumbnailUrl(m.thumbnail_path) : undefined,
type: m.media_type,
caption: m.title,
tags: m.tags || []
}))}
currentIndex={currentIndex}
onSelect={(index) => {
setCurrentIndex(index);
setShowGrid(false);
}}
onClose={() => setShowGrid(false)}
/>
)}
</div>
);
};