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