feat: 添加素材导入时的模特绑定功能

- 在 CreateMaterialRequest 中添加 model_id 字段
- 更新 Material 实体添加 new_with_model 方法支持创建时绑定模特
- 修改 MaterialService 和 AsyncMaterialService 支持导入时模特绑定
- 在 MaterialImportDialog 中添加模特选择下拉框
- 支持在导入素材时可选择绑定到特定模特
- 遵循 Tauri 开发规范的组件设计和业务逻辑分层
This commit is contained in:
imeepos 2025-07-15 12:59:27 +08:00
parent b86a8a5c23
commit 6f888295bb
5 changed files with 97 additions and 5 deletions

View File

@ -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,
);
// 保存到数据库

View File

@ -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,
);
// 保存到数据库

View File

@ -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;

View File

@ -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>
)}

View File

@ -96,6 +96,7 @@ export interface CreateMaterialRequest {
file_paths: string[];
auto_process: boolean;
max_segment_duration?: number;
model_id?: string; // 可选的模特绑定ID
}
export interface MaterialProcessingConfig {