feat: 实现项目-模板绑定和素材-模特绑定管理功能
新功能: - 项目-模板绑定管理系统 - 支持主要/次要模板绑定类型 - 绑定优先级和状态管理 - 批量绑定操作 - 绑定关系的CRUD操作 - 素材-模特绑定管理系统 - 素材与模特的关联管理 - 批量绑定/解绑操作 - 绑定统计和分析 - 素材编辑对话框 架构改进: - 新增项目-模板绑定数据模型和仓库层 - 新增素材-模特绑定业务服务层 - 完善的API命令层实现 - 响应式前端界面设计 用户体验优化: - 统一的通知系统 - 增强的加载状态组件 - 流畅的交互动画 - 优雅的确认对话框 测试覆盖: - 单元测试和集成测试 - 业务逻辑验证 - API接口测试 技术栈: - 后端: Rust + Tauri + SQLite - 前端: React + TypeScript + TailwindCSS - 状态管理: Zustand - 测试: Vitest + Rust测试框架 配置更新: - 更新数据库迁移脚本 - 完善测试配置 - 优化构建流程
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user