feat: 实现项目-模板绑定和素材-模特绑定管理功能

新功能:
- 项目-模板绑定管理系统
  - 支持主要/次要模板绑定类型
  - 绑定优先级和状态管理
  - 批量绑定操作
  - 绑定关系的CRUD操作

- 素材-模特绑定管理系统
  - 素材与模特的关联管理
  - 批量绑定/解绑操作
  - 绑定统计和分析
  - 素材编辑对话框

 架构改进:
- 新增项目-模板绑定数据模型和仓库层
- 新增素材-模特绑定业务服务层
- 完善的API命令层实现
- 响应式前端界面设计

 用户体验优化:
- 统一的通知系统
- 增强的加载状态组件
- 流畅的交互动画
- 优雅的确认对话框

 测试覆盖:
- 单元测试和集成测试
- 业务逻辑验证
- API接口测试

 技术栈:
- 后端: Rust + Tauri + SQLite
- 前端: React + TypeScript + TailwindCSS
- 状态管理: Zustand
- 测试: Vitest + Rust测试框架

 配置更新:
- 更新数据库迁移脚本
- 完善测试配置
- 优化构建流程
This commit is contained in:
imeepos
2025-07-15 12:50:30 +08:00
parent 9f222b562d
commit 730402aba0
41 changed files with 7259 additions and 49 deletions

View File

@@ -1,14 +1,17 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import {
FileVideo, FileAudio, FileImage, File, Clock, ExternalLink, ChevronDown, ChevronUp,
Monitor, Volume2, Palette, Calendar, Hash, Zap, HardDrive, Film, Eye, Brain, Loader2
Monitor, Volume2, Palette, Calendar, Hash, Zap, HardDrive, Film, Eye, Brain, Loader2, User, Edit2
} from 'lucide-react';
import { Material, MaterialSegment } from '../types/material';
import { useMaterialStore } from '../store/materialStore';
import { useVideoClassificationStore } from '../store/videoClassificationStore';
import { Model } from '../types/model';
import { invoke } from '@tauri-apps/api/core';
interface MaterialCardProps {
material: Material;
onEdit?: (material: Material) => void;
}
// 格式化时间(秒转为 mm:ss 格式)
@@ -57,13 +60,15 @@ const formatDate = (dateString: string): string => {
* 素材卡片组件
* 显示素材信息和切分片段
*/
export const MaterialCard: React.FC<MaterialCardProps> = ({ material }) => {
export const MaterialCard: React.FC<MaterialCardProps> = ({ material, onEdit }) => {
const { getMaterialSegments } = useMaterialStore();
const { startClassification, isLoading: classificationLoading } = useVideoClassificationStore();
const [segments, setSegments] = useState<MaterialSegment[]>([]);
const [showSegments, setShowSegments] = useState(false);
const [loadingSegments, setLoadingSegments] = useState(false);
const [isClassifying, setIsClassifying] = useState(false);
const [associatedModel, setAssociatedModel] = useState<Model | null>(null);
const [loadingModel, setLoadingModel] = useState(false);
// 获取素材类型图标
const getTypeIcon = (type: string) => {
@@ -95,7 +100,28 @@ export const MaterialCard: React.FC<MaterialCardProps> = ({ material }) => {
}
};
// 获取关联的模特信息
useEffect(() => {
const fetchAssociatedModel = async () => {
if (!material.model_id) {
setAssociatedModel(null);
return;
}
setLoadingModel(true);
try {
const model = await invoke<Model>('get_model', { id: material.model_id });
setAssociatedModel(model);
} catch (error) {
console.error('获取关联模特失败:', error);
setAssociatedModel(null);
} finally {
setLoadingModel(false);
}
};
fetchAssociatedModel();
}, [material.model_id]);
// 加载切分片段
const loadSegments = async () => {
@@ -191,9 +217,20 @@ export const MaterialCard: React.FC<MaterialCardProps> = ({ material }) => {
{getTypeIcon(material.material_type)}
<h4 className="font-medium text-gray-900 truncate">{material.name}</h4>
</div>
<span className={`px-2 py-1 text-xs rounded-full ${getStatusColor(material.processing_status)}`}>
{material.processing_status}
</span>
<div className="flex items-center space-x-2">
{onEdit && (
<button
onClick={() => onEdit(material)}
className="text-gray-400 hover:text-blue-500 transition-colors"
title="编辑素材"
>
<Edit2 className="w-4 h-4" />
</button>
)}
<span className={`px-2 py-1 text-xs rounded-full ${getStatusColor(material.processing_status)}`}>
{material.processing_status}
</span>
</div>
</div>
{/* 素材详细信息 */}
@@ -210,6 +247,34 @@ export const MaterialCard: React.FC<MaterialCardProps> = ({ material }) => {
</div>
</div>
{/* 关联模特信息 */}
{material.model_id && (
<div className="bg-purple-50 rounded-lg p-3">
<div className="flex items-center space-x-2">
<User className="w-4 h-4 text-purple-600" />
<span className="text-sm font-medium text-purple-700"></span>
</div>
{loadingModel ? (
<div className="flex items-center space-x-2 mt-2">
<Loader2 className="w-3 h-3 animate-spin text-purple-600" />
<span className="text-xs text-purple-600">...</span>
</div>
) : associatedModel ? (
<div className="mt-2 space-y-1">
<p className="text-sm font-medium text-gray-900">{associatedModel.name}</p>
{associatedModel.stage_name && (
<p className="text-xs text-gray-600">: {associatedModel.stage_name}</p>
)}
{associatedModel.description && (
<p className="text-xs text-gray-600">{associatedModel.description}</p>
)}
</div>
) : (
<p className="text-xs text-red-600 mt-2"></p>
)}
</div>
)}
{/* 元数据信息 */}
{material.metadata !== 'None' && (
<div className="space-y-2">