Files
mixvideo-v2/apps/desktop/src/components/MaterialEditDialog.tsx
imeepos 887f5de793 feat: Complete Modal and Tab UI/UX optimization
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.
2025-07-15 20:09:54 +08:00

247 lines
9.2 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 { 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>
);
};