Enhanced Modal Components: - Optimized DeleteConfirmDialog with beautiful gradients and improved layout - Enhanced MaterialEditDialog with modern design and better information hierarchy - Improved TemplateDetailModal with elegant header and refined tab navigation - Enhanced AiClassificationFormDialog with modern styling Unified Tab System: - Created reusable TabNavigation component with multiple variants (default, pills, underline) - Implemented consistent tab design across ProjectDetails and other pages - Added support for icons, counts, and disabled states in tabs - Improved accessibility and keyboard navigation Advanced Animations: - Added comprehensive modal animations (fade-in, scale-in, slide-in) - Enhanced backdrop blur effects and smooth transitions - Implemented proper enter/exit animations for better UX - Added reduced motion support for accessibility Responsive Design: - Optimized modal layouts for mobile, tablet, and desktop - Improved touch-friendly interactions for mobile devices - Enhanced modal sizing and positioning across screen sizes - Added proper scrolling and overflow handling Interaction Improvements: - Enhanced ESC key support for modal closing - Improved backdrop click handling - Better focus management and keyboard navigation - Consistent button styling and hover effects All modal and tab components now follow unified design language while maintaining full functionality.
247 lines
9.2 KiB
TypeScript
247 lines
9.2 KiB
TypeScript
/**
|
||
* 素材编辑对话框组件
|
||
* 用于编辑素材信息,包括模特绑定
|
||
*/
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { X, Save, User } from 'lucide-react';
|
||
import { Material } from '../types/material';
|
||
import { Model } from '../types/model';
|
||
import { CustomSelect } from './CustomSelect';
|
||
import { LoadingSpinner } from './LoadingSpinner';
|
||
import { ErrorMessage } from './ErrorMessage';
|
||
import { invoke } from '@tauri-apps/api/core';
|
||
|
||
interface MaterialEditDialogProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
material: Material | null;
|
||
onSave: (materialId: string, updates: Partial<Material>) => Promise<void>;
|
||
}
|
||
|
||
export const MaterialEditDialog: React.FC<MaterialEditDialogProps> = ({
|
||
isOpen,
|
||
onClose,
|
||
material,
|
||
onSave,
|
||
}) => {
|
||
const [models, setModels] = useState<Model[]>([]);
|
||
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
||
const [loading, setLoading] = useState(false);
|
||
const [saving, setSaving] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
// 加载模特列表
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
loadModels();
|
||
}
|
||
}, [isOpen]);
|
||
|
||
// 初始化选中的模特
|
||
useEffect(() => {
|
||
if (material) {
|
||
setSelectedModelId(material.model_id || '');
|
||
}
|
||
}, [material]);
|
||
|
||
const loadModels = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const modelList = await invoke<Model[]>('get_all_models');
|
||
setModels(modelList);
|
||
} catch (error) {
|
||
console.error('加载模特列表失败:', error);
|
||
setError('加载模特列表失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
if (!material) return;
|
||
|
||
setSaving(true);
|
||
setError(null);
|
||
try {
|
||
const updates: Partial<Material> = {
|
||
model_id: selectedModelId || undefined,
|
||
};
|
||
|
||
await onSave(material.id, updates);
|
||
onClose();
|
||
} catch (error) {
|
||
console.error('保存素材失败:', error);
|
||
setError(error instanceof Error ? error.message : '保存失败');
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const getModelOptions = () => {
|
||
const options = [
|
||
{ value: '', label: '无关联模特' },
|
||
...models.map(model => ({
|
||
value: model.id,
|
||
label: model.stage_name || model.name,
|
||
description: model.description,
|
||
})),
|
||
];
|
||
return options;
|
||
};
|
||
|
||
const getSelectedModel = () => {
|
||
return models.find(m => m.id === selectedModelId);
|
||
};
|
||
|
||
if (!isOpen || !material) return null;
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm flex items-center justify-center z-50 animate-fade-in">
|
||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 animate-scale-in overflow-hidden">
|
||
{/* 美观的头部 */}
|
||
<div className="flex items-center justify-between p-6 bg-gradient-to-r from-primary-50 to-primary-100 border-b border-primary-200">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl flex items-center justify-center shadow-sm">
|
||
<User className="w-5 h-5 text-white" />
|
||
</div>
|
||
<h2 className="text-xl font-bold text-gray-900">编辑素材</h2>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-white hover:shadow-sm rounded-lg transition-all duration-200"
|
||
disabled={saving}
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* 美观的内容区域 */}
|
||
<div className="p-6 space-y-6">
|
||
{/* 错误信息 */}
|
||
{error && <ErrorMessage message={error} />}
|
||
|
||
{/* 优化的素材信息 */}
|
||
<div className="space-y-3">
|
||
<h3 className="text-sm font-semibold text-gray-800 flex items-center gap-2">
|
||
<div className="w-2 h-2 bg-primary-500 rounded-full"></div>
|
||
素材信息
|
||
</h3>
|
||
<div className="bg-gradient-to-r from-gray-50 to-gray-100 rounded-xl p-4 border border-gray-200">
|
||
<p className="font-semibold text-gray-900 mb-2 truncate" title={material.name}>
|
||
{material.name}
|
||
</p>
|
||
<div className="flex items-center gap-4 text-xs text-gray-600">
|
||
<span className="px-2 py-1 bg-white rounded-lg border">
|
||
类型: {material.material_type}
|
||
</span>
|
||
<span className="px-2 py-1 bg-white rounded-lg border">
|
||
状态: {material.processing_status}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 优化的模特绑定 */}
|
||
<div className="space-y-3">
|
||
<label className="block text-sm font-semibold text-gray-800">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||
<User className="w-4 h-4" />
|
||
<span>关联模特</span>
|
||
</div>
|
||
</label>
|
||
|
||
{loading ? (
|
||
<div className="flex items-center justify-center py-6 bg-gray-50 rounded-xl">
|
||
<LoadingSpinner size="small" />
|
||
<span className="ml-3 text-sm text-gray-600">加载模特列表...</span>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
<CustomSelect
|
||
value={selectedModelId}
|
||
onChange={setSelectedModelId}
|
||
options={getModelOptions()}
|
||
placeholder="选择关联的模特"
|
||
disabled={saving}
|
||
className="w-full"
|
||
/>
|
||
|
||
{selectedModelId && getSelectedModel() && (
|
||
<div className="p-3 bg-green-50 border border-green-200 rounded-xl">
|
||
<div className="flex items-start gap-3">
|
||
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
|
||
<User className="w-4 h-4 text-green-600" />
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-green-800">
|
||
{getSelectedModel()?.name}
|
||
</p>
|
||
{getSelectedModel()?.description && (
|
||
<p className="text-xs text-green-700 mt-1">
|
||
{getSelectedModel()?.description}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 优化的说明 */}
|
||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-xl p-4">
|
||
<div className="flex items-start gap-3">
|
||
<div className="w-5 h-5 bg-blue-100 rounded-lg flex items-center justify-center mt-0.5">
|
||
<span className="text-blue-600 text-xs font-bold">💡</span>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm font-medium text-blue-800 mb-1">使用提示</p>
|
||
<p className="text-xs text-blue-700 leading-relaxed">
|
||
为素材绑定模特后,可以更好地进行素材分类和管理。
|
||
如果素材中没有出现任何模特,可以选择"无关联模特"。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 美观的操作按钮 */}
|
||
<div className="px-6 py-4 bg-gradient-to-r from-gray-50 to-gray-100 border-t border-gray-200">
|
||
<div className="flex justify-end gap-3">
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="px-4 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-all duration-200"
|
||
disabled={saving}
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleSave}
|
||
className="px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md min-w-[100px]"
|
||
disabled={saving || loading}
|
||
>
|
||
{saving ? (
|
||
<div className="flex items-center justify-center gap-2">
|
||
<LoadingSpinner size="small" />
|
||
<span>保存中...</span>
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center justify-center gap-2">
|
||
<Save className="w-4 h-4" />
|
||
<span>保存</span>
|
||
</div>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|