diff --git a/src/family/pages/MediaLibrary.tsx b/src/family/pages/MediaLibrary.tsx new file mode 100644 index 0000000..9208958 --- /dev/null +++ b/src/family/pages/MediaLibrary.tsx @@ -0,0 +1,684 @@ +import React, { useState, useEffect } from 'react'; +import { Upload, Image as ImageIcon, Video, X } from 'lucide-react'; +import * as mediaService from '../services/mediaService'; + +/** + * 家属端媒体库界面 + * 上传、打标、配置触发策略 + */ +export const MediaLibrary: React.FC = () => { + const [selectedMedia, setSelectedMedia] = useState(null); + const [showUploader, setShowUploader] = useState(false); + const [mediaItems, setMediaItems] = useState([]); + const [loading, setLoading] = useState(true); + const [uploading, setUploading] = useState(false); + const [uploadFile, setUploadFile] = useState(null); + const [uploadPreviewUrl, setUploadPreviewUrl] = useState(null); + const [uploadTitle, setUploadTitle] = useState(''); + const [uploadDescription, setUploadDescription] = useState(''); + const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null); + + // 编辑表单状态 + const [editTitle, setEditTitle] = useState(''); + const [editTags, setEditTags] = useState([]); + const [newTag, setNewTag] = useState(''); + const [editTimeWindows, setEditTimeWindows] = useState([]); + const [editMoods, setEditMoods] = useState([]); + const [editOccasion, setEditOccasion] = useState(''); + const [editCooldown, setEditCooldown] = useState(60); + + const familyId = 'family_001'; // 实际使用时从用户上下文获取 + + // 加载媒体列表 + useEffect(() => { + loadMedia(); + }, []); + + // 当选中媒体时,初始化编辑表单 + useEffect(() => { + if (selectedMedia) { + setEditTitle(selectedMedia.title); + setEditTags(selectedMedia.tags || []); + setEditTimeWindows(selectedMedia.time_windows || []); + setEditMoods(selectedMedia.moods || []); + setEditOccasion(selectedMedia.occasions?.[0] || ''); + setEditCooldown(selectedMedia.cooldown || 60); + } + }, [selectedMedia]); + + const loadMedia = async () => { + try { + setLoading(true); + const data = await mediaService.getFamilyMedia(familyId); + setMediaItems(data); + } catch (error) { + console.error('加载媒体列表失败:', error); + showToast('加载媒体列表失败', 'error'); + } finally { + setLoading(false); + } + }; + + const showToast = (message: string, type: 'success' | 'error' | 'warning') => { + setToast({ message, type }); + setTimeout(() => setToast(null), 3000); + }; + + // 处理文件选择 + const handleFileSelect = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + setUploadFile(file); + + // 创建预览URL + const previewUrl = URL.createObjectURL(file); + setUploadPreviewUrl(previewUrl); + + // 自动填充标题(使用文件名,去掉扩展名) + if (!uploadTitle) { + const fileName = file.name.replace(/\.[^/.]+$/, ''); + setUploadTitle(fileName); + } + } + }; + + // 清理预览URL以避免内存泄漏 + useEffect(() => { + return () => { + if (uploadPreviewUrl) { + URL.revokeObjectURL(uploadPreviewUrl); + } + }; + }, [uploadPreviewUrl]); + + // 处理上传 + const handleUpload = async () => { + if (!uploadFile || !uploadTitle.trim()) { + showToast('请选择文件并填写标题', 'warning'); + return; + } + + try { + setUploading(true); + await mediaService.uploadMedia({ + file: uploadFile, + family_id: familyId, + title: uploadTitle, + description: uploadDescription, + }); + + showToast('上传成功', 'success'); + setShowUploader(false); + setUploadFile(null); + setUploadPreviewUrl(null); + setUploadTitle(''); + setUploadDescription(''); + + // 重新加载媒体列表 + await loadMedia(); + } catch (error) { + console.error('上传失败:', error); + showToast(error instanceof Error ? error.message : '上传失败', 'error'); + } finally { + setUploading(false); + } + }; + + // 保存媒体策略 + const handleSavePolicy = async () => { + if (!selectedMedia) return; + + try { + await mediaService.updateMedia(selectedMedia.id, { + title: editTitle, + tags: editTags, + time_windows: editTimeWindows, + moods: editMoods, + occasions: editOccasion ? [editOccasion] : [], + cooldown: editCooldown, + priority: selectedMedia.priority || 5, + }); + + showToast('保存成功', 'success'); + setSelectedMedia(null); + await loadMedia(); + } catch (error) { + console.error('保存失败:', error); + showToast(error instanceof Error ? error.message : '保存失败', 'error'); + } + }; + + // 添加标签 + const handleAddTag = () => { + if (newTag.trim() && !editTags.includes(newTag.trim())) { + setEditTags([...editTags, newTag.trim()]); + setNewTag(''); + } + }; + + // 删除标签 + const handleRemoveTag = (tagToRemove: string) => { + setEditTags(editTags.filter(tag => tag !== tagToRemove)); + }; + + // 切换时段 + const handleToggleTimeWindow = (timeWindow: string) => { + if (editTimeWindows.includes(timeWindow)) { + setEditTimeWindows(editTimeWindows.filter(tw => tw !== timeWindow)); + } else { + setEditTimeWindows([...editTimeWindows, timeWindow]); + } + }; + + // 切换心境 + const handleToggleMood = (mood: string) => { + if (editMoods.includes(mood)) { + setEditMoods(editMoods.filter(m => m !== mood)); + } else { + setEditMoods([...editMoods, mood]); + } + }; + + // 删除媒体 + const handleDeleteMedia = async (mediaId: number) => { + if (!confirm('确定要删除这个媒体吗?')) return; + + try { + await mediaService.deleteMedia(mediaId); + showToast('删除成功', 'success'); + setSelectedMedia(null); + await loadMedia(); + } catch (error) { + console.error('删除失败:', error); + showToast(error instanceof Error ? error.message : '删除失败', 'error'); + } + }; + + return ( + <> +
+ {/* 顶部导航 */} +
+
+
+

媒体库

+ +
+
+
+ + {/* 主要内容区 */} +
+ {/* 步骤提示 */} +
+
+ {['上传', '打标', '触发策略'].map((step, index) => ( +
+
+
+ {index + 1} +
+ {step} +
+ {index < 2 && ( +
+ )} +
+ ))} +
+
+ + {/* 加载状态 */} + {loading && ( +
+
+

加载中...

+
+ )} + + {/* 媒体网格 */} + {!loading && ( +
+ {mediaItems.map((item) => ( +
setSelectedMedia(item)} + > + {/* 缩略图 */} +
+ {item.media_type === 'photo' ? ( + <> + {item.title} { + // 加载失败时显示占位图标 + e.currentTarget.style.display = 'none'; + e.currentTarget.nextElementSibling?.classList.remove('hidden'); + }} + /> + {/* 加载失败时的占位图标 */} +
+ +
+ + ) : ( + // 视频显示缩略图或图标 + item.thumbnail_path ? ( + <> + {item.title} { + e.currentTarget.style.display = 'none'; + e.currentTarget.nextElementSibling?.classList.remove('hidden'); + }} + /> +
+
+ {/* 播放图标叠加 */} +
+
+
+
+
+ + ) : ( +
+
+ ) + )} +
+ {item.media_type === 'photo' ? '照片' : '视频'} +
+ {/* 播放次数 */} + {item.play_count > 0 && ( +
+ 播放 {item.play_count} 次 +
+ )} +
+ + {/* 信息区 */} +
+

{item.title}

+
+ {item.tags.map((tag, index) => ( + + {tag} + + ))} +
+

+ 上传于 {new Date(item.created_at).toLocaleDateString()} +

+
+
+ ))} + + {/* 上传占位卡 */} +
setShowUploader(true)} + className="card p-0 overflow-hidden cursor-pointer hover:shadow-xl transition-shadow border-2 border-dashed border-gray-300 hover:border-primary-400" + > +
+ + 点击上传 +
+
+
+ )} +
+ + {/* 媒体详情侧边栏 */} + {selectedMedia && ( +
+
+
+

媒体详情与策略

+ +
+ +
+ {/* 基本信息 */} +
+ + setEditTitle(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" + /> +
+ + {/* 人物标签 */} +
+ +
+ {editTags.map((tag, index) => ( + + {tag} + + + ))} +
+
+ setNewTag(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddTag(); + } + }} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" + /> + +
+
+ + {/* 触发策略 */} +
+

触发策略

+ + {/* 时段选择 */} +
+ +
+ {[ + { label: '早餐后 (07:00-09:00)', value: '07:00-09:00' }, + { label: '午后 (14:00-17:00)', value: '14:00-17:00' }, + { label: '晚间 (19:00-21:00)', value: '19:00-21:00' }, + { label: '睡前 (21:00-22:00)', value: '21:00-22:00' }, + ].map((time, index) => ( + + ))} +
+
+ + {/* 心境选择 */} +
+ +
+ {[ + { label: '开心', value: 'happy' }, + { label: '平静', value: 'calm' }, + { label: '难过', value: 'sad' }, + { label: '焦虑', value: 'anxious' }, + ].map((mood, index) => ( + + ))} +
+
+ + {/* 特殊场合 */} +
+ + +
+ + {/* 冷却时间 */} +
+ + +
+
+ + {/* 保存和删除按钮 */} +
+ + +
+
+
+
+ )} + + {/* 上传对话框 */} + {showUploader && ( +
+
+
+

上传媒体

+ +
+ + {/* 文件选择和预览 */} +
+ +
+ + {/* 标题输入 */} +
+ + setUploadTitle(e.target.value)} + placeholder="例如:小米生日视频" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" + /> +
+ + {/* 描述输入 */} +
+ +