Add File
This commit is contained in:
684
src/family/pages/MediaLibrary.tsx
Normal file
684
src/family/pages/MediaLibrary.tsx
Normal file
@@ -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<mediaService.Media | null>(null);
|
||||
const [showUploader, setShowUploader] = useState(false);
|
||||
const [mediaItems, setMediaItems] = useState<mediaService.Media[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||
const [uploadPreviewUrl, setUploadPreviewUrl] = useState<string | null>(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<string[]>([]);
|
||||
const [newTag, setNewTag] = useState('');
|
||||
const [editTimeWindows, setEditTimeWindows] = useState<string[]>([]);
|
||||
const [editMoods, setEditMoods] = useState<string[]>([]);
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* 顶部导航 */}
|
||||
<div className="bg-white border-b">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-gray-900">媒体库</h1>
|
||||
<button
|
||||
onClick={() => setShowUploader(true)}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Upload size={18} />
|
||||
上传媒体
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主要内容区 */}
|
||||
<div className="max-w-7xl mx-auto px-6 py-8">
|
||||
{/* 步骤提示 */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between max-w-3xl mx-auto">
|
||||
{['上传', '打标', '触发策略'].map((step, index) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-600 text-white flex items-center justify-center font-bold">
|
||||
{index + 1}
|
||||
</div>
|
||||
<span className="text-sm mt-2 text-gray-700">{step}</span>
|
||||
</div>
|
||||
{index < 2 && (
|
||||
<div className="w-32 h-1 bg-primary-200 mx-4" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">加载中...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 媒体网格 */}
|
||||
{!loading && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{mediaItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="card p-0 overflow-hidden cursor-pointer hover:shadow-xl transition-shadow"
|
||||
onClick={() => setSelectedMedia(item)}
|
||||
>
|
||||
{/* 缩略图 */}
|
||||
<div className="bg-gray-200 aspect-video flex items-center justify-center relative overflow-hidden">
|
||||
{item.media_type === 'photo' ? (
|
||||
<>
|
||||
<img
|
||||
src={mediaService.getMediaUrl(item.file_path)}
|
||||
alt={item.title}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
// 加载失败时显示占位图标
|
||||
e.currentTarget.style.display = 'none';
|
||||
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
||||
}}
|
||||
/>
|
||||
{/* 加载失败时的占位图标 */}
|
||||
<div className="hidden absolute inset-0 flex items-center justify-center bg-gray-200">
|
||||
<ImageIcon size={48} className="text-gray-400" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// 视频显示缩略图或图标
|
||||
item.thumbnail_path ? (
|
||||
<>
|
||||
<img
|
||||
src={mediaService.getThumbnailUrl(item.thumbnail_path)}
|
||||
alt={item.title}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
||||
}}
|
||||
/>
|
||||
<div className="hidden absolute inset-0 flex items-center justify-center bg-gradient-to-br from-purple-100 to-blue-100">
|
||||
<Video size={64} className="text-purple-500" />
|
||||
</div>
|
||||
{/* 播放图标叠加 */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-12 h-12 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
|
||||
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-white border-b-8 border-b-transparent ml-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-purple-100 to-blue-100">
|
||||
<Video size={64} className="text-purple-500" />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<div className="absolute top-2 right-2 px-2 py-1 bg-black bg-opacity-60 text-white text-xs rounded">
|
||||
{item.media_type === 'photo' ? '照片' : '视频'}
|
||||
</div>
|
||||
{/* 播放次数 */}
|
||||
{item.play_count > 0 && (
|
||||
<div className="absolute bottom-2 left-2 px-2 py-1 bg-black bg-opacity-60 text-white text-xs rounded">
|
||||
播放 {item.play_count} 次
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 信息区 */}
|
||||
<div className="p-4">
|
||||
<h3 className="font-medium text-gray-900 mb-2">{item.title}</h3>
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{item.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
上传于 {new Date(item.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 上传占位卡 */}
|
||||
<div
|
||||
onClick={() => 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"
|
||||
>
|
||||
<div className="aspect-video flex flex-col items-center justify-center text-gray-400 hover:text-primary-600 transition-colors">
|
||||
<Upload size={48} />
|
||||
<span className="mt-2 text-sm font-medium">点击上传</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 媒体详情侧边栏 */}
|
||||
{selectedMedia && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex justify-end animate-fade-in">
|
||||
<div className="bg-white w-full max-w-2xl h-full overflow-y-auto">
|
||||
<div className="sticky top-0 bg-white border-b p-6 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-gray-900">媒体详情与策略</h2>
|
||||
<button
|
||||
onClick={() => setSelectedMedia(null)}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 基本信息 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
标题
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editTitle}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 人物标签 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
人物/场景标签
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{editTags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm flex items-center gap-1"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
className="hover:text-blue-900"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="添加标签..."
|
||||
value={newTag}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddTag}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 触发策略 */}
|
||||
<div className="border-t pt-6">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">触发策略</h3>
|
||||
|
||||
{/* 时段选择 */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
播放时段
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ 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
|
||||
key={index}
|
||||
className={`flex items-center gap-2 p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||
editTimeWindows.includes(time.value)
|
||||
? 'bg-blue-50 border-blue-300'
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={editTimeWindows.includes(time.value)}
|
||||
onChange={() => handleToggleTimeWindow(time.value)}
|
||||
/>
|
||||
<span className="text-sm">{time.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 心境选择 */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
适合心境
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ label: '开心', value: 'happy' },
|
||||
{ label: '平静', value: 'calm' },
|
||||
{ label: '难过', value: 'sad' },
|
||||
{ label: '焦虑', value: 'anxious' },
|
||||
].map((mood, index) => (
|
||||
<label
|
||||
key={index}
|
||||
className={`flex items-center gap-2 p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||
editMoods.includes(mood.value)
|
||||
? 'bg-purple-50 border-purple-300'
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={editMoods.includes(mood.value)}
|
||||
onChange={() => handleToggleMood(mood.value)}
|
||||
/>
|
||||
<span className="text-sm">{mood.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 特殊场合 */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
特殊场合
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||
value={editOccasion}
|
||||
onChange={(e) => setEditOccasion(e.target.value)}
|
||||
>
|
||||
<option value="">无</option>
|
||||
<option value="birthday">生日</option>
|
||||
<option value="anniversary">纪念日</option>
|
||||
<option value="medication_reward">服药后奖励</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 冷却时间 */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
冷却时间(避免重复打扰)
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||
value={editCooldown}
|
||||
onChange={(e) => setEditCooldown(Number(e.target.value))}
|
||||
>
|
||||
<option value="30">30 分钟</option>
|
||||
<option value="60">1 小时</option>
|
||||
<option value="120">2 小时</option>
|
||||
<option value="1440">1 天</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 保存和删除按钮 */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => handleDeleteMedia(selectedMedia.id)}
|
||||
className="px-6 py-3 border border-red-300 text-red-600 rounded-lg hover:bg-red-50 transition-colors font-medium"
|
||||
>
|
||||
删除媒体
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSavePolicy}
|
||||
className="flex-1 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium"
|
||||
>
|
||||
保存策略
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 上传对话框 */}
|
||||
{showUploader && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-6 animate-fade-in">
|
||||
<div className="bg-white rounded-2xl max-w-2xl w-full p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold text-gray-900">上传媒体</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUploader(false);
|
||||
setUploadFile(null);
|
||||
setUploadPreviewUrl(null);
|
||||
setUploadTitle('');
|
||||
setUploadDescription('');
|
||||
}}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 文件选择和预览 */}
|
||||
<div className="mb-4">
|
||||
<label className="border-2 border-dashed border-gray-300 rounded-xl overflow-hidden hover:border-primary-400 transition-colors cursor-pointer block">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
{uploadFile && uploadPreviewUrl ? (
|
||||
<div>
|
||||
{/* 预览区域 */}
|
||||
<div className="bg-gray-100 aspect-video flex items-center justify-center relative overflow-hidden">
|
||||
{uploadFile.type.startsWith('image/') ? (
|
||||
<img
|
||||
src={uploadPreviewUrl}
|
||||
alt="预览"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
) : uploadFile.type.startsWith('video/') ? (
|
||||
<video
|
||||
src={uploadPreviewUrl}
|
||||
className="w-full h-full object-contain"
|
||||
controls
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{/* 文件信息 */}
|
||||
<div className="p-4 bg-white">
|
||||
<p className="text-primary-600 font-medium mb-1">{uploadFile.name}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{(uploadFile.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-12 text-center">
|
||||
<Upload size={48} className="mx-auto text-gray-400 mb-4" />
|
||||
<p className="text-gray-700 font-medium mb-1">
|
||||
点击选择文件
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
支持 JPG、PNG、MP4 格式,单个文件不超过 100MB
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 标题输入 */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
标题 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={uploadTitle}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 描述输入 */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
描述
|
||||
</label>
|
||||
<textarea
|
||||
value={uploadDescription}
|
||||
onChange={(e) => setUploadDescription(e.target.value)}
|
||||
placeholder="添加一些描述信息..."
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUploader(false);
|
||||
setUploadFile(null);
|
||||
setUploadPreviewUrl(null);
|
||||
setUploadTitle('');
|
||||
setUploadDescription('');
|
||||
}}
|
||||
className="flex-1 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
disabled={uploading}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
disabled={uploading || !uploadFile || !uploadTitle.trim()}
|
||||
className="flex-1 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{uploading ? '上传中...' : '开始上传'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toast 提示 */}
|
||||
{toast && (
|
||||
<div className="fixed bottom-4 right-4 z-50 animate-fade-in">
|
||||
<div
|
||||
className={`px-6 py-3 rounded-lg shadow-lg text-white ${
|
||||
toast.type === 'success'
|
||||
? 'bg-green-600'
|
||||
: toast.type === 'error'
|
||||
? 'bg-red-600'
|
||||
: 'bg-yellow-600'
|
||||
}`}
|
||||
>
|
||||
{toast.message}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user