mixvideo-v2/apps/desktop/src/components/MaterialSegmentModelDialog.tsx

273 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { Users, X, Search, Plus, Check } from 'lucide-react';
import { SegmentWithDetails } from '../types/materialSegmentView';
import { Model } from '../types/model';
import { invoke } from '@tauri-apps/api/core';
interface MaterialSegmentModelDialogProps {
segments: SegmentWithDetails[];
isOpen: boolean;
onClose: () => void;
onConfirm: (modelId: string | null) => void;
isLoading?: boolean;
}
/**
* MaterialSegment模特关联对话框组件
* 遵循 Tauri 开发规范的组件设计模式
*/
export const MaterialSegmentModelDialog: React.FC<MaterialSegmentModelDialogProps> = ({
segments,
isOpen,
onClose,
onConfirm,
isLoading = false,
}) => {
const [models, setModels] = useState<Model[]>([]);
const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [loadingModels, setLoadingModels] = useState(false);
// 加载模特列表
useEffect(() => {
if (isOpen) {
loadModels();
}
}, [isOpen]);
const loadModels = async () => {
setLoadingModels(true);
try {
const modelList = await invoke<Model[]>('get_all_models');
setModels(modelList);
} catch (error) {
console.error('加载模特列表失败:', error);
} finally {
setLoadingModels(false);
}
};
if (!isOpen) return null;
const isBatch = segments.length > 1;
const currentModels = [...new Set(segments.map(s => s.model?.id).filter(Boolean))];
// 过滤模特列表
const filteredModels = models.filter(model =>
model.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
model.id.toLowerCase().includes(searchTerm.toLowerCase())
);
// 格式化时长
const formatDuration = (seconds: number) => {
const minutes = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, '0')}`;
};
const handleConfirm = () => {
onConfirm(selectedModelId);
};
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-lg w-full max-h-[90vh] overflow-hidden">
{/* 对话框头部 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<Users className="w-5 h-5 text-green-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">
{isBatch ? '批量关联模特' : '关联模特'}
</h3>
<p className="text-sm text-gray-600">
{isBatch ? `${segments.length} 个片段关联模特` : '为该片段关联模特'}
</p>
</div>
</div>
<button
onClick={onClose}
disabled={isLoading}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
>
<X className="w-5 h-5" />
</button>
</div>
{/* 对话框内容 */}
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
{/* 片段概览 */}
<div className="mb-6">
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="bg-gray-50 rounded-lg p-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<span className="font-medium text-gray-900">{segments.length}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<span className="font-medium text-gray-900">
{formatDuration(segments.reduce((sum, s) => sum + s.segment.duration, 0))}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<span className="font-medium text-green-600">
{segments.filter(s => s.model).length}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<span className="font-medium text-orange-600">
{segments.filter(s => !s.model).length}
</span>
</div>
</div>
</div>
{/* 当前关联的模特 */}
{currentModels.length > 0 && (
<div className="mt-4">
<h5 className="text-xs font-medium text-gray-700 mb-2"></h5>
<div className="flex flex-wrap gap-2">
{currentModels.map(modelId => {
const model = models.find(m => m.id === modelId);
return (
<span key={modelId} className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">
{model?.name || modelId?.slice(-8)}
</span>
);
})}
</div>
</div>
)}
</div>
{/* 模特选择 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900"></h4>
<button
onClick={() => setSelectedModelId(null)}
className={`text-xs px-2 py-1 rounded transition-colors ${
selectedModelId === null
? 'bg-red-100 text-red-700'
: 'bg-gray-100 text-gray-600 hover:bg-red-100 hover:text-red-700'
}`}
>
</button>
</div>
{/* 搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="搜索模特名称或ID..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent text-sm"
/>
</div>
{/* 模特列表 */}
<div className="space-y-2 max-h-64 overflow-y-auto">
{loadingModels ? (
<div className="text-center py-8">
<div className="w-6 h-6 border-2 border-green-500 border-t-transparent rounded-full animate-spin mx-auto mb-2" />
<p className="text-sm text-gray-600">...</p>
</div>
) : filteredModels.length > 0 ? (
filteredModels.map((model) => (
<div
key={model.id}
onClick={() => setSelectedModelId(model.id)}
className={`p-3 border rounded-lg cursor-pointer transition-all ${
selectedModelId === model.id
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-green-300 hover:bg-green-50'
}`}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2">
<h5 className="text-sm font-medium text-gray-900">{model.name}</h5>
{selectedModelId === model.id && (
<Check className="w-4 h-4 text-green-600" />
)}
</div>
<div className="flex items-center space-x-4 mt-1 text-xs text-gray-600">
<span>ID: {model.id.slice(-8)}</span>
<span>: {model.gender}</span>
</div>
</div>
</div>
</div>
))
) : (
<div className="text-center py-8">
<Users className="w-8 h-8 text-gray-300 mx-auto mb-2" />
<p className="text-sm text-gray-600">
{searchTerm ? '未找到匹配的模特' : '暂无可用模特'}
</p>
{!searchTerm && (
<button className="mt-2 text-sm text-green-600 hover:text-green-800 flex items-center mx-auto">
<Plus className="w-4 h-4 mr-1" />
</button>
)}
</div>
)}
</div>
</div>
{/* 操作说明 */}
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="text-sm text-blue-800">
<p className="font-medium mb-1"></p>
<ul className="list-disc list-inside space-y-1 text-xs">
<li></li>
<li></li>
<li>"取消关联"</li>
<li>AI分类结果</li>
</ul>
</div>
</div>
</div>
{/* 对话框底部 */}
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200 bg-gray-50">
<button
onClick={onClose}
disabled={isLoading}
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors disabled:opacity-50"
>
</button>
<button
onClick={handleConfirm}
disabled={isLoading}
className="inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
...
</>
) : (
<>
<Users className="w-4 h-4 mr-2" />
{selectedModelId ? '关联模特' : '取消关联'}
</>
)}
</button>
</div>
</div>
</div>
);
};