feat: 添加素材导入时的模特绑定功能
- 在 CreateMaterialRequest 中添加 model_id 字段 - 更新 Material 实体添加 new_with_model 方法支持创建时绑定模特 - 修改 MaterialService 和 AsyncMaterialService 支持导入时模特绑定 - 在 MaterialImportDialog 中添加模特选择下拉框 - 支持在导入素材时可选择绑定到特定模特 - 遵循 Tauri 开发规范的组件设计和业务逻辑分层
This commit is contained in:
parent
b86a8a5c23
commit
6f888295bb
|
|
@ -77,6 +77,7 @@ impl AsyncMaterialService {
|
|||
let file_path_clone = file_path.clone();
|
||||
let config_clone = config.clone();
|
||||
let event_bus_clone = Arc::clone(&event_bus);
|
||||
let model_id_clone = request.model_id.clone();
|
||||
|
||||
match Self::process_single_file_async(
|
||||
repository_clone,
|
||||
|
|
@ -84,6 +85,7 @@ impl AsyncMaterialService {
|
|||
&file_path_clone,
|
||||
&config_clone,
|
||||
event_bus_clone,
|
||||
model_id_clone,
|
||||
).await {
|
||||
Ok(Some(material)) => {
|
||||
info!(
|
||||
|
|
@ -153,6 +155,7 @@ impl AsyncMaterialService {
|
|||
file_path: &str,
|
||||
config: &MaterialProcessingConfig,
|
||||
event_bus: Arc<EventBusManager>,
|
||||
model_id: Option<String>,
|
||||
) -> Result<Option<Material>> {
|
||||
let _timer = PERFORMANCE_MONITOR.start_operation("async_process_single_file");
|
||||
|
||||
|
|
@ -190,14 +193,15 @@ impl AsyncMaterialService {
|
|||
// 确定素材类型
|
||||
let material_type = MaterialType::from_extension(extension);
|
||||
|
||||
// 创建素材对象
|
||||
let material = Material::new(
|
||||
// 创建素材对象(带模特绑定)
|
||||
let material = Material::new_with_model(
|
||||
project_id.to_string(),
|
||||
file_name.clone(),
|
||||
file_path.to_string(),
|
||||
file_size,
|
||||
md5_hash,
|
||||
material_type,
|
||||
model_id,
|
||||
);
|
||||
|
||||
// 保存到数据库
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ impl MaterialService {
|
|||
for file_path in &request.file_paths {
|
||||
debug!(file_path = %file_path, "处理文件");
|
||||
|
||||
match Self::process_single_file(repository, &request.project_id, file_path, config) {
|
||||
match Self::process_single_file(repository, &request.project_id, file_path, config, request.model_id.clone()) {
|
||||
Ok(Some(material)) => {
|
||||
info!(
|
||||
file_path = %file_path,
|
||||
|
|
@ -98,6 +98,7 @@ impl MaterialService {
|
|||
project_id: &str,
|
||||
file_path: &str,
|
||||
config: &MaterialProcessingConfig,
|
||||
model_id: Option<String>,
|
||||
) -> Result<Option<Material>> {
|
||||
let _timer = PERFORMANCE_MONITOR.start_operation("process_single_file");
|
||||
|
||||
|
|
@ -132,14 +133,15 @@ impl MaterialService {
|
|||
// 确定素材类型
|
||||
let material_type = MaterialType::from_extension(extension);
|
||||
|
||||
// 创建素材对象
|
||||
let material = Material::new(
|
||||
// 创建素材对象(带模特绑定)
|
||||
let material = Material::new_with_model(
|
||||
project_id.to_string(),
|
||||
file_name,
|
||||
file_path.to_string(),
|
||||
file_size,
|
||||
md5_hash,
|
||||
material_type,
|
||||
model_id,
|
||||
);
|
||||
|
||||
// 保存到数据库
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ pub struct CreateMaterialRequest {
|
|||
pub file_paths: Vec<String>,
|
||||
pub auto_process: bool,
|
||||
pub max_segment_duration: Option<f64>, // 最大片段时长(秒)
|
||||
pub model_id: Option<String>, // 可选的模特绑定ID
|
||||
}
|
||||
|
||||
/// 视频切分模式
|
||||
|
|
@ -241,6 +242,37 @@ impl Material {
|
|||
}
|
||||
}
|
||||
|
||||
/// 创建新的素材实例(带模特绑定)
|
||||
pub fn new_with_model(
|
||||
project_id: String,
|
||||
name: String,
|
||||
original_path: String,
|
||||
file_size: u64,
|
||||
md5_hash: String,
|
||||
material_type: MaterialType,
|
||||
model_id: Option<String>,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
project_id,
|
||||
model_id,
|
||||
name,
|
||||
original_path,
|
||||
file_size,
|
||||
md5_hash,
|
||||
material_type,
|
||||
processing_status: ProcessingStatus::Pending,
|
||||
metadata: MaterialMetadata::None,
|
||||
scene_detection: None,
|
||||
segments: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
processed_at: None,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新处理状态
|
||||
pub fn update_status(&mut self, status: ProcessingStatus, error_message: Option<String>) {
|
||||
self.processing_status = status;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { X, Upload, FileText, AlertCircle, CheckCircle, Loader2 } from 'lucide-r
|
|||
import { listen } from '@tauri-apps/api/event';
|
||||
import { useMaterialStore } from '../store/materialStore';
|
||||
import { CreateMaterialRequest, MaterialImportResult } from '../types/material';
|
||||
import { Model } from '../types/model';
|
||||
import { MaterialModelBindingService } from '../services/materialModelBindingService';
|
||||
import { CustomSelect } from './CustomSelect';
|
||||
|
||||
interface MaterialImportDialogProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -46,6 +49,11 @@ export const MaterialImportDialog: React.FC<MaterialImportDialogProps> = ({
|
|||
const [recursiveScan, setRecursiveScan] = useState(true);
|
||||
const [selectedFileTypes, setSelectedFileTypes] = useState<string[]>([]);
|
||||
|
||||
// 模特绑定相关状态
|
||||
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
||||
const [availableModels, setAvailableModels] = useState<Model[]>([]);
|
||||
const [loadingModels, setLoadingModels] = useState(false);
|
||||
|
||||
// 检查 FFmpeg 可用性
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
|
|
@ -58,10 +66,30 @@ export const MaterialImportDialog: React.FC<MaterialImportDialogProps> = ({
|
|||
if (isOpen) {
|
||||
setSelectedFiles([]);
|
||||
setStep('select');
|
||||
setSelectedModelId('');
|
||||
clearError();
|
||||
}
|
||||
}, [isOpen, clearError]);
|
||||
|
||||
// 加载可用模特
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadAvailableModels();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const loadAvailableModels = async () => {
|
||||
setLoadingModels(true);
|
||||
try {
|
||||
const models = await MaterialModelBindingService.getAllModels();
|
||||
setAvailableModels(models.filter(model => model.is_active));
|
||||
} catch (error) {
|
||||
console.error('加载模特列表失败:', error);
|
||||
} finally {
|
||||
setLoadingModels(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置事件监听器用于接收导入进度更新
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
|
@ -179,6 +207,7 @@ export const MaterialImportDialog: React.FC<MaterialImportDialogProps> = ({
|
|||
file_paths: selectedFiles,
|
||||
auto_process: autoProcess,
|
||||
max_segment_duration: maxSegmentDuration,
|
||||
model_id: selectedModelId || undefined,
|
||||
};
|
||||
|
||||
console.log('开始异步导入:', request);
|
||||
|
|
@ -390,6 +419,30 @@ export const MaterialImportDialog: React.FC<MaterialImportDialogProps> = ({
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 模特绑定选择 */}
|
||||
<div>
|
||||
<label htmlFor="modelSelect" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
绑定模特(可选)
|
||||
</label>
|
||||
<CustomSelect
|
||||
value={selectedModelId}
|
||||
onChange={setSelectedModelId}
|
||||
options={[
|
||||
{ value: '', label: '不绑定模特' },
|
||||
...availableModels.map(model => ({
|
||||
value: model.id,
|
||||
label: model.stage_name || model.name,
|
||||
})),
|
||||
]}
|
||||
placeholder={loadingModels ? '加载中...' : '选择模特'}
|
||||
disabled={loadingModels}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
选择模特后,导入的素材将自动绑定到该模特
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ export interface CreateMaterialRequest {
|
|||
file_paths: string[];
|
||||
auto_process: boolean;
|
||||
max_segment_duration?: number;
|
||||
model_id?: string; // 可选的模特绑定ID
|
||||
}
|
||||
|
||||
export interface MaterialProcessingConfig {
|
||||
|
|
|
|||
Loading…
Reference in New Issue