268 lines
8.2 KiB
TypeScript
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>
|
|
);
|
|
};
|