fix: 添加导入素材
This commit is contained in:
parent
0b322d06fb
commit
c51ede77f7
|
|
@ -0,0 +1,231 @@
|
|||
# 项目素材导入功能使用指南
|
||||
|
||||
## 🎯 功能概述
|
||||
|
||||
项目素材导入功能允许用户为特定项目批量导入视频素材,支持选择模特关联、自动标签和自动分镜功能,大大提升素材管理效率。
|
||||
|
||||
## 🚀 功能特点
|
||||
|
||||
### 1. 灵活的文件选择
|
||||
- **单文件/多文件选择**: 支持选择一个或多个视频文件
|
||||
- **文件夹批量导入**: 选择整个文件夹进行批量导入
|
||||
- **格式支持**: 支持 MP4、AVI、MOV、MKV、WMV、FLV、WebM、M4V 等主流视频格式
|
||||
|
||||
### 2. 智能关联功能
|
||||
- **项目自动关联**: 导入的素材自动关联到当前项目
|
||||
- **模特标签关联**: 可选择关联的模特,自动添加模特标签
|
||||
- **项目标签**: 自动添加项目商品名作为标签
|
||||
|
||||
### 3. 自动分镜功能
|
||||
- **智能分镜**: 自动将长视频分割成多个片段
|
||||
- **场景识别**: 基于场景变化进行分镜
|
||||
- **便于编辑**: 分镜后的片段更适合后续编辑使用
|
||||
|
||||
### 4. 标签管理
|
||||
- **自定义标签**: 可添加自定义标签
|
||||
- **自动标签**: 系统自动添加项目和模特相关标签
|
||||
- **标签预览**: 实时预览将要添加的标签
|
||||
|
||||
## 📍 使用位置
|
||||
|
||||
在项目详情页面的素材管理区域,点击右上角的"导入素材"按钮即可打开导入功能。
|
||||
|
||||
## 🔧 使用步骤
|
||||
|
||||
### 步骤1: 打开导入功能
|
||||
1. 进入项目详情页面
|
||||
2. 在素材管理区域找到"导入素材"按钮
|
||||
3. 点击按钮打开导入模态框
|
||||
|
||||
### 步骤2: 选择素材文件
|
||||
有两种选择方式:
|
||||
|
||||
#### 方式A: 选择单个或多个文件
|
||||
1. 点击"选择文件"按钮
|
||||
2. 在文件选择对话框中选择视频文件
|
||||
3. 支持按住Ctrl/Cmd键多选文件
|
||||
|
||||
#### 方式B: 选择整个文件夹
|
||||
1. 点击"选择文件夹"按钮
|
||||
2. 选择包含视频文件的文件夹
|
||||
3. 系统会自动扫描文件夹中的所有支持格式的视频文件
|
||||
|
||||
### 步骤3: 配置导入选项
|
||||
|
||||
#### 关联模特(可选)
|
||||
- 勾选要关联的模特
|
||||
- 选中的模特标签会自动添加到导入的素材中
|
||||
- 有助于后续按模特筛选素材
|
||||
|
||||
#### 添加标签(可选)
|
||||
- 在标签输入框中输入自定义标签
|
||||
- 按回车键或点击"+"按钮添加标签
|
||||
- 可添加多个标签,点击标签上的删除按钮可移除
|
||||
|
||||
#### 自动分镜设置
|
||||
- 默认启用自动分镜功能
|
||||
- 取消勾选可禁用自动分镜
|
||||
- 启用后长视频会被自动分割成多个片段
|
||||
|
||||
### 步骤4: 开始导入
|
||||
1. 确认所有配置无误
|
||||
2. 点击"开始导入"按钮
|
||||
3. 等待导入完成
|
||||
|
||||
## 📊 导入过程
|
||||
|
||||
### 1. 文件验证
|
||||
- 检查文件格式是否支持
|
||||
- 验证文件完整性
|
||||
- 过滤不支持的文件
|
||||
|
||||
### 2. 素材处理
|
||||
- 提取视频元数据(时长、分辨率、帧率等)
|
||||
- 生成MD5哈希值防重复
|
||||
- 创建缩略图
|
||||
|
||||
### 3. 自动分镜(如果启用)
|
||||
- 分析视频场景变化
|
||||
- 智能确定分镜点
|
||||
- 生成多个视频片段
|
||||
|
||||
### 4. 标签处理
|
||||
- 添加项目相关标签
|
||||
- 添加模特相关标签
|
||||
- 添加用户自定义标签
|
||||
|
||||
### 5. 数据存储
|
||||
- 保存素材信息到数据库
|
||||
- 建立项目关联关系
|
||||
- 更新素材统计
|
||||
|
||||
## 📋 导入结果
|
||||
|
||||
导入完成后会显示详细的结果信息:
|
||||
|
||||
### 成功信息
|
||||
- **总文件数**: 尝试导入的文件总数
|
||||
- **成功导入**: 成功导入的文件数量
|
||||
- **生成片段**: 自动分镜生成的片段数量
|
||||
- **跳过文件**: 因重复或其他原因跳过的文件
|
||||
- **失败文件**: 导入失败的文件数量
|
||||
|
||||
### 错误信息
|
||||
如果有文件导入失败,会显示具体的错误信息,包括:
|
||||
- 文件名
|
||||
- 失败原因
|
||||
- 建议解决方案
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
### 1. 文件组织
|
||||
- **统一命名**: 使用有意义的文件名
|
||||
- **分类存放**: 按类型或用途组织文件夹
|
||||
- **避免特殊字符**: 文件名避免使用特殊字符
|
||||
|
||||
### 2. 标签策略
|
||||
- **描述性标签**: 使用描述性的标签名称
|
||||
- **统一规范**: 团队内部统一标签命名规范
|
||||
- **分层标签**: 使用分层的标签体系
|
||||
|
||||
**推荐标签示例**:
|
||||
```
|
||||
类型标签: 产品展示、人物介绍、场景拍摄
|
||||
风格标签: 商务、休闲、时尚、科技
|
||||
用途标签: 主视频、背景素材、转场素材
|
||||
```
|
||||
|
||||
### 3. 模特关联
|
||||
- **准确关联**: 确保选择正确的模特
|
||||
- **完整关联**: 包含该模特的素材都应该关联
|
||||
- **及时更新**: 新增模特后及时关联相关素材
|
||||
|
||||
### 4. 自动分镜
|
||||
- **长视频启用**: 对于超过1分钟的视频建议启用
|
||||
- **短视频禁用**: 对于短视频可以禁用以保持完整性
|
||||
- **后期调整**: 可在导入后手动调整分镜结果
|
||||
|
||||
## 🔍 故障排除
|
||||
|
||||
### 问题1: 文件选择失败
|
||||
|
||||
**可能原因**:
|
||||
- 文件路径包含特殊字符
|
||||
- 文件被其他程序占用
|
||||
- 权限不足
|
||||
|
||||
**解决方案**:
|
||||
1. 检查文件路径,避免特殊字符
|
||||
2. 关闭可能占用文件的程序
|
||||
3. 确保有足够的文件访问权限
|
||||
|
||||
### 问题2: 导入失败
|
||||
|
||||
**可能原因**:
|
||||
- 文件格式不支持
|
||||
- 文件损坏
|
||||
- 磁盘空间不足
|
||||
- 网络连接问题
|
||||
|
||||
**解决方案**:
|
||||
1. 确认文件格式在支持列表中
|
||||
2. 检查文件完整性
|
||||
3. 确保有足够的磁盘空间
|
||||
4. 检查网络连接状态
|
||||
|
||||
### 问题3: 自动分镜效果不佳
|
||||
|
||||
**可能原因**:
|
||||
- 视频场景变化不明显
|
||||
- 视频质量较低
|
||||
- 分镜参数不合适
|
||||
|
||||
**解决方案**:
|
||||
1. 对于场景单一的视频可禁用自动分镜
|
||||
2. 提高视频质量
|
||||
3. 手动调整分镜结果
|
||||
|
||||
### 问题4: 标签丢失
|
||||
|
||||
**可能原因**:
|
||||
- 导入过程中断
|
||||
- 标签格式错误
|
||||
- 系统异常
|
||||
|
||||
**解决方案**:
|
||||
1. 重新导入素材
|
||||
2. 检查标签格式
|
||||
3. 手动添加丢失的标签
|
||||
|
||||
## 🚀 高级功能
|
||||
|
||||
### 批量操作
|
||||
- 支持同时导入多个文件夹
|
||||
- 支持批量设置标签和模特关联
|
||||
- 支持批量启用/禁用自动分镜
|
||||
|
||||
### 导入模板
|
||||
- 保存常用的导入配置
|
||||
- 快速应用预设配置
|
||||
- 团队共享导入模板
|
||||
|
||||
### 进度监控
|
||||
- 实时显示导入进度
|
||||
- 支持暂停和恢复导入
|
||||
- 详细的处理日志
|
||||
|
||||
## 📈 性能优化
|
||||
|
||||
### 导入速度优化
|
||||
- **小批量导入**: 避免一次导入过多文件
|
||||
- **网络稳定**: 确保网络连接稳定
|
||||
- **系统资源**: 确保系统有足够的内存和CPU资源
|
||||
|
||||
### 存储优化
|
||||
- **定期清理**: 定期清理不需要的素材
|
||||
- **压缩设置**: 合理设置视频压缩参数
|
||||
- **分布存储**: 大量素材可考虑分布式存储
|
||||
|
||||
---
|
||||
|
||||
通过项目素材导入功能,您可以高效地管理项目素材,提升视频制作效率!
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
/**
|
||||
* 导入素材模态框组件
|
||||
* 参考素材管理页面设计,支持选择目录、模特和自动分镜配置
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { X, Upload, FolderOpen, Users, Scissors, Video, CheckCircle, Plus, Trash2 } from 'lucide-react'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { Project } from '../services/projectService'
|
||||
import { Model } from '../services/modelService'
|
||||
import { MediaService, BatchUploadResult } from '../services/mediaService'
|
||||
|
||||
interface ImportMaterialModalProps {
|
||||
project: Project
|
||||
models: Model[]
|
||||
onConfirm: (result: BatchUploadResult) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const ImportMaterialModal: React.FC<ImportMaterialModalProps> = ({
|
||||
project,
|
||||
models,
|
||||
onConfirm,
|
||||
onCancel
|
||||
}) => {
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [uploadResult, setUploadResult] = useState<BatchUploadResult | null>(null)
|
||||
const [selectedFiles, setSelectedFiles] = useState<string[]>([])
|
||||
const [selectedDirectory, setSelectedDirectory] = useState<string>('')
|
||||
const [selectedModelIds, setSelectedModelIds] = useState<string[]>([])
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
const [newTag, setNewTag] = useState('')
|
||||
const [enableAutoSegmentation, setEnableAutoSegmentation] = useState(true)
|
||||
|
||||
// 选择文件
|
||||
const selectFiles = async () => {
|
||||
try {
|
||||
const files = await open({
|
||||
multiple: true,
|
||||
title: '选择视频文件',
|
||||
filters: [
|
||||
{
|
||||
name: '视频文件',
|
||||
extensions: ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv', 'webm', 'm4v']
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
if (files && Array.isArray(files)) {
|
||||
setSelectedFiles(files)
|
||||
setSelectedDirectory('')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select files:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择目录
|
||||
const selectDirectory = async () => {
|
||||
try {
|
||||
const directory = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: '选择视频文件夹'
|
||||
})
|
||||
|
||||
if (directory && typeof directory === 'string') {
|
||||
setSelectedDirectory(directory)
|
||||
setSelectedFiles([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select directory:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加标签
|
||||
const addTag = () => {
|
||||
if (newTag.trim() && !tags.includes(newTag.trim())) {
|
||||
setTags([...tags, newTag.trim()])
|
||||
setNewTag('')
|
||||
}
|
||||
}
|
||||
|
||||
// 移除标签
|
||||
const removeTag = (tagToRemove: string) => {
|
||||
setTags(tags.filter(tag => tag !== tagToRemove))
|
||||
}
|
||||
|
||||
// 处理导入
|
||||
const handleImport = async () => {
|
||||
if (selectedFiles.length === 0 && !selectedDirectory) {
|
||||
alert('请先选择文件或文件夹')
|
||||
return
|
||||
}
|
||||
|
||||
setUploading(true)
|
||||
setUploadResult(null)
|
||||
|
||||
try {
|
||||
// 准备导入参数
|
||||
const importTags = [...tags]
|
||||
|
||||
// 添加项目标签
|
||||
if (project.product_name && !importTags.includes(project.product_name)) {
|
||||
importTags.push(project.product_name)
|
||||
}
|
||||
|
||||
// 添加模特标签
|
||||
const selectedModels = models.filter(m => selectedModelIds.includes(m.id))
|
||||
selectedModels.forEach(model => {
|
||||
if (!importTags.includes(model.model_number)) {
|
||||
importTags.push(model.model_number)
|
||||
}
|
||||
})
|
||||
|
||||
let result: BatchUploadResult
|
||||
|
||||
if (selectedDirectory) {
|
||||
// 批量导入文件夹
|
||||
const response = await MediaService.batchUploadVideoFiles({
|
||||
source_directory: selectedDirectory,
|
||||
tags: importTags.length > 0 ? importTags : undefined
|
||||
})
|
||||
|
||||
if (response.status && response.data) {
|
||||
result = response.data
|
||||
} else {
|
||||
throw new Error(response.msg || '批量导入失败')
|
||||
}
|
||||
} else {
|
||||
// 导入单个或多个文件
|
||||
result = {
|
||||
total_files: selectedFiles.length,
|
||||
uploaded_files: 0,
|
||||
failed_files: 0,
|
||||
skipped_files: 0,
|
||||
total_segments: 0,
|
||||
uploaded_list: [],
|
||||
skipped_list: [],
|
||||
failed_list: []
|
||||
}
|
||||
|
||||
for (const filePath of selectedFiles) {
|
||||
try {
|
||||
const response = await MediaService.uploadVideoFile({
|
||||
source_path: filePath,
|
||||
tags: importTags
|
||||
})
|
||||
|
||||
if (response.status && response.data) {
|
||||
result.uploaded_files++
|
||||
result.total_segments += response.data.segments.length
|
||||
result.uploaded_list.push(response.data)
|
||||
} else {
|
||||
result.failed_files++
|
||||
result.failed_list.push({
|
||||
filename: filePath.split('/').pop() || filePath,
|
||||
error: response.msg || '上传失败'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
result.failed_files++
|
||||
result.failed_list.push({
|
||||
filename: filePath.split('/').pop() || filePath,
|
||||
error: String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setUploadResult(result)
|
||||
|
||||
// 如果有成功的上传,调用完成回调
|
||||
if (result.uploaded_files > 0) {
|
||||
onConfirm(result)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Import failed:', error)
|
||||
alert('导入失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
{/* 标题栏 */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">导入素材</h2>
|
||||
<p className="text-sm text-gray-600">为项目 "{project.name}" 导入视频素材</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 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-3">选择素材</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={selectFiles}
|
||||
disabled={uploading}
|
||||
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Video className="w-8 h-8 text-gray-400 mb-2" />
|
||||
<span className="text-sm font-medium text-gray-700">选择文件</span>
|
||||
<span className="text-xs text-gray-500">支持多选视频文件</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={selectDirectory}
|
||||
disabled={uploading}
|
||||
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<FolderOpen className="w-8 h-8 text-gray-400 mb-2" />
|
||||
<span className="text-sm font-medium text-gray-700">选择文件夹</span>
|
||||
<span className="text-xs text-gray-500">批量导入整个文件夹</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 显示选择的文件或目录 */}
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex items-center text-blue-700">
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
<span className="text-sm font-medium">已选择 {selectedFiles.length} 个文件</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedDirectory && (
|
||||
<div className="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex items-center text-blue-700">
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
<span className="text-sm font-medium">已选择文件夹</span>
|
||||
</div>
|
||||
<div className="text-xs text-blue-600 mt-1 truncate">{selectedDirectory}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 模特选择 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
<Users className="w-4 h-4 inline mr-1" />
|
||||
关联模特(可选)
|
||||
</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 max-h-32 overflow-y-auto">
|
||||
{models.map((model) => (
|
||||
<label key={model.id} className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedModelIds.includes(model.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedModelIds([...selectedModelIds, model.id])
|
||||
} else {
|
||||
setSelectedModelIds(selectedModelIds.filter(id => id !== model.id))
|
||||
}
|
||||
}}
|
||||
disabled={uploading}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{model.model_number}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标签设置 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">标签(可选)</label>
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
value={newTag}
|
||||
onChange={(e) => setNewTag(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && addTag()}
|
||||
placeholder="输入标签名称"
|
||||
disabled={uploading}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<button
|
||||
onClick={addTag}
|
||||
disabled={!newTag.trim() || uploading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => removeTag(tag)}
|
||||
disabled={uploading}
|
||||
className="ml-2 text-gray-500 hover:text-gray-700 disabled:opacity-50"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 自动分镜设置 */}
|
||||
<div>
|
||||
<label className="flex items-center mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableAutoSegmentation}
|
||||
onChange={(e) => setEnableAutoSegmentation(e.target.checked)}
|
||||
disabled={uploading}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Scissors className="w-4 h-4 mr-1" />
|
||||
<span className="text-sm font-medium text-gray-700">启用自动分镜</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 ml-6">
|
||||
自动将长视频分割成多个片段,便于后续编辑使用
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 上传结果 */}
|
||||
{uploadResult && (
|
||||
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div className="flex items-center text-green-700 mb-2">
|
||||
<CheckCircle className="w-5 h-5 mr-2" />
|
||||
<span className="font-medium">导入完成</span>
|
||||
</div>
|
||||
<div className="text-sm text-green-600 space-y-1">
|
||||
<div>总文件数: {uploadResult.total_files}</div>
|
||||
<div>成功导入: {uploadResult.uploaded_files}</div>
|
||||
<div>失败: {uploadResult.failed_files}</div>
|
||||
<div>跳过: {uploadResult.skipped_files}</div>
|
||||
<div>生成片段: {uploadResult.total_segments}</div>
|
||||
</div>
|
||||
{uploadResult.failed_list.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-sm font-medium text-red-700">错误信息:</div>
|
||||
<div className="text-xs text-red-600 max-h-20 overflow-y-auto">
|
||||
{uploadResult.failed_list.map((error, index) => (
|
||||
<div key={index}>• {error.filename}: {error.error}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className="flex items-center justify-between p-6 border-t border-gray-200">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
disabled={uploading}
|
||||
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
{uploadResult ? '关闭' : '取消'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={uploading || (selectedFiles.length === 0 && !selectedDirectory)}
|
||||
className="flex items-center px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{uploading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
导入中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
开始导入
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImportMaterialModal
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { Video, Play, Search, Filter, Tag, Clock, HardDrive, Eye, User, Folder } from 'lucide-react'
|
||||
import { Video, Play, Search, Filter, Tag, Clock, HardDrive, Eye, User, Folder, Upload } from 'lucide-react'
|
||||
import { Project } from '../services/projectService'
|
||||
import { VideoSegment, MediaService } from '../services/mediaService'
|
||||
import { Model } from '../services/modelService'
|
||||
import { ResourceCategory, ResourceCategoryService } from '../services/resourceCategoryService'
|
||||
import { BatchUploadResult } from '../services/mediaService'
|
||||
import VideoPlayer from './VideoPlayer'
|
||||
import ImportMaterialModal from './ImportMaterialModal'
|
||||
|
||||
interface ProjectMaterialsCenterProps {
|
||||
project: Project
|
||||
|
|
@ -25,6 +27,7 @@ const ProjectMaterialsCenter: React.FC<ProjectMaterialsCenterProps> = ({
|
|||
const [usageFilter, setUsageFilter] = useState<'all' | 'used' | 'unused'>('all')
|
||||
const [selectedMaterial, setSelectedMaterial] = useState<VideoSegment | null>(null)
|
||||
const [showVideoPlayer, setShowVideoPlayer] = useState(false)
|
||||
const [showImportModal, setShowImportModal] = useState(false)
|
||||
|
||||
// 素材分类相关状态
|
||||
const [categories, setCategories] = useState<ResourceCategory[]>([])
|
||||
|
|
@ -153,6 +156,18 @@ const ProjectMaterialsCenter: React.FC<ProjectMaterialsCenterProps> = ({
|
|||
setShowVideoPlayer(true)
|
||||
}
|
||||
|
||||
// 处理导入完成
|
||||
const handleImportComplete = (result: BatchUploadResult) => {
|
||||
setShowImportModal(false)
|
||||
|
||||
// 刷新素材列表
|
||||
onMaterialsChange([...materials])
|
||||
|
||||
// 显示导入结果
|
||||
const message = `导入完成!\n成功: ${result.uploaded_files} 个文件\n生成片段: ${result.total_segments} 个`
|
||||
alert(message)
|
||||
}
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const remainingSeconds = Math.floor(seconds % 60)
|
||||
|
|
@ -186,9 +201,18 @@ const ProjectMaterialsCenter: React.FC<ProjectMaterialsCenterProps> = ({
|
|||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-gray-900">项目素材</h2>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="text-sm text-gray-600">
|
||||
{filteredMaterials.length} / {materials.length} 个素材
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowImportModal(true)}
|
||||
className="flex items-center px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Upload size={14} className="mr-1" />
|
||||
导入素材
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选器 - 紧凑布局 */}
|
||||
|
|
@ -468,6 +492,16 @@ const ProjectMaterialsCenter: React.FC<ProjectMaterialsCenterProps> = ({
|
|||
}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 导入素材模态框 */}
|
||||
{showImportModal && (
|
||||
<ImportMaterialModal
|
||||
project={project}
|
||||
models={models}
|
||||
onConfirm={handleImportComplete}
|
||||
onCancel={() => setShowImportModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue