Add File
This commit is contained in:
300
src/elderly/components/MediaPlayer.tsx
Normal file
300
src/elderly/components/MediaPlayer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user