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

268 lines
8.2 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { useProjectStore } from '../store/projectStore';
import { ProjectFormData, ProjectFormErrors } from '../types/project';
import { InteractiveInput } from './InteractiveInput';
import { InteractiveButton } from './InteractiveButton';
import { Folder, X } from 'lucide-react';
interface ProjectFormProps {
initialData?: Partial<ProjectFormData>;
onSubmit: (data: ProjectFormData) => Promise<void>;
onCancel: () => void;
isEdit?: boolean;
}
/**
* 项目表单组件
* 遵循 Tauri 开发规范的表单设计模式
*/
export const ProjectForm: React.FC<ProjectFormProps> = ({
initialData,
onSubmit,
onCancel,
isEdit = false
}) => {
const { isLoading, validateProjectPath, getDefaultProjectName } = useProjectStore();
const [formData, setFormData] = useState<ProjectFormData>({
name: initialData?.name || '',
path: initialData?.path || '',
description: initialData?.description || ''
});
const [errors, setErrors] = useState<ProjectFormErrors>({});
const [isValidatingPath, setIsValidatingPath] = useState(false);
// 验证表单
const validateForm = (): boolean => {
const newErrors: ProjectFormErrors = {};
if (!formData.name.trim()) {
newErrors.name = '项目名称不能为空';
} else if (formData.name.length > 100) {
newErrors.name = '项目名称不能超过100个字符';
}
if (!formData.path.trim()) {
newErrors.path = '项目路径不能为空';
}
if (formData.description.length > 500) {
newErrors.description = '项目描述不能超过500个字符';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 选择目录
const handleSelectDirectory = async () => {
try {
const selectedPath = await invoke<string | null>('select_directory');
if (selectedPath) {
setFormData(prev => ({ ...prev, path: selectedPath }));
// 如果项目名称为空,尝试从路径获取默认名称
if (!formData.name.trim()) {
try {
const defaultName = await getDefaultProjectName(selectedPath);
if (defaultName) {
setFormData(prev => ({ ...prev, name: defaultName }));
}
} catch (error) {
console.warn('获取默认项目名称失败:', error);
}
}
}
} catch (error) {
console.error('选择目录失败:', error);
}
};
// 验证路径
const handlePathValidation = async (path: string) => {
if (!path.trim()) return;
setIsValidatingPath(true);
try {
const isValid = await validateProjectPath(path);
if (!isValid) {
setErrors(prev => ({
...prev,
path: '无效的项目路径或没有访问权限'
}));
} else {
setErrors(prev => {
const { path: _, ...rest } = prev;
return rest;
});
}
} catch (error) {
setErrors(prev => ({
...prev,
path: '路径验证失败'
}));
} finally {
setIsValidatingPath(false);
}
};
// 路径变化时验证
useEffect(() => {
if (formData.path && !isEdit) {
const timeoutId = setTimeout(() => {
handlePathValidation(formData.path);
}, 500);
return () => clearTimeout(timeoutId);
}
}, [formData.path, isEdit]);
// 处理表单提交
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
try {
await onSubmit(formData);
} catch (error) {
console.error('提交表单失败:', error);
}
};
return (
<div className="modal-overlay">
<div className="modal modal-lg">
{/* 模态框头部 */}
<div className="modal-header">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary-100 text-primary-600 rounded-xl">
<Folder size={20} />
</div>
<div>
<h2 className="text-xl font-semibold text-gray-900">
{isEdit ? '编辑项目' : '新建项目'}
</h2>
<p className="text-sm text-gray-600">
{isEdit ? '修改项目信息' : '创建一个新的视频项目'}
</p>
</div>
</div>
<button className="modal-close" onClick={onCancel}>
<X size={20} />
</button>
</div>
{/* 模态框内容 */}
<form onSubmit={handleSubmit} className="modal-body space-y-6">
{/* 项目名称 */}
<InteractiveInput
label="项目名称"
value={formData.name}
onChange={(value) => setFormData(prev => ({ ...prev, name: value }))}
placeholder="为您的项目起一个好听的名字"
error={errors.name}
maxLength={100}
required
autoFocus
/>
{/* 项目路径 */}
<div className="space-y-3">
<div className="flex gap-3">
<div className="flex-1">
<InteractiveInput
label="项目路径"
value={formData.path}
onChange={(value) => setFormData(prev => ({ ...prev, path: value }))}
placeholder="选择项目存储位置"
error={errors.path}
success={!errors.path && formData.path && !isValidatingPath ? "路径有效" : undefined}
loading={isValidatingPath}
disabled={isEdit}
required
/>
</div>
{!isEdit && (
<div className="flex items-end">
<InteractiveButton
variant="secondary"
size="md"
onClick={handleSelectDirectory}
icon={<Folder size={16} />}
className="whitespace-nowrap"
>
</InteractiveButton>
</div>
)}
</div>
<div className="form-hint">
{isEdit ? '编辑模式下无法修改项目路径' : '选择一个空文件夹或新建文件夹作为项目目录'}
</div>
</div>
{/* 项目描述 */}
<div className="form-group">
<label htmlFor="description" className="form-label">
</label>
<textarea
id="description"
className={`form-textarea ${errors.description ? 'error' : ''}`}
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
placeholder="描述一下这个项目的用途和内容(可选)"
rows={4}
maxLength={500}
/>
<div className="flex justify-between items-center">
<div className="form-hint">
</div>
<div className="text-xs text-gray-500">
{formData.description.length}/500
</div>
</div>
{errors.description && (
<div className="form-error">
<span className="text-red-600">
{errors.description}
</span>
</div>
)}
</div>
</form>
{/* 模态框底部 */}
<div className="modal-footer">
<InteractiveButton
variant="secondary"
size="md"
onClick={onCancel}
disabled={isLoading}
fullWidth
>
</InteractiveButton>
<InteractiveButton
type="submit"
variant="primary"
size="md"
onClick={handleSubmit}
disabled={isLoading || isValidatingPath || Object.keys(errors).length > 0}
loading={isLoading}
fullWidth
className="shadow-glow"
>
{isEdit ? '保存更改' : '创建项目'}
</InteractiveButton>
</div>
</div>
</div>
);
};