281 lines
9.4 KiB
TypeScript
281 lines
9.4 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||
import { Switch } from '@/components/ui/switch';
|
||
import { Separator } from '@/components/ui/separator';
|
||
import { FileText, Tag, Folder, AlignLeft, Copy, Loader2 } from 'lucide-react';
|
||
import { useToast } from '@/components/ui/use-toast';
|
||
import { createDefinition } from '../service';
|
||
import { getEnabledCategories } from '../../Category/service';
|
||
import type { FormDefinitionRequest } from '../types';
|
||
import type { FormCategoryResponse } from '../../Category/types';
|
||
|
||
interface CreateModalProps {
|
||
visible: boolean;
|
||
onClose: () => void;
|
||
onSuccess: (id: number) => void;
|
||
}
|
||
|
||
/**
|
||
* 表单定义创建弹窗(第一步:输入基本信息)
|
||
*/
|
||
const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }) => {
|
||
const navigate = useNavigate();
|
||
const { toast } = useToast();
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
|
||
const [formData, setFormData] = useState({
|
||
name: '',
|
||
key: '',
|
||
categoryId: undefined as number | undefined,
|
||
description: '',
|
||
isTemplate: false,
|
||
});
|
||
|
||
// 加载分类列表
|
||
useEffect(() => {
|
||
if (visible) {
|
||
loadCategories();
|
||
}
|
||
}, [visible]);
|
||
|
||
const loadCategories = async () => {
|
||
try {
|
||
const result = await getEnabledCategories();
|
||
setCategories(result || []);
|
||
} catch (error) {
|
||
console.error('加载分类失败:', error);
|
||
}
|
||
};
|
||
|
||
// 重置表单
|
||
const resetForm = () => {
|
||
setFormData({
|
||
name: '',
|
||
key: '',
|
||
categoryId: undefined,
|
||
description: '',
|
||
isTemplate: false,
|
||
});
|
||
};
|
||
|
||
// 关闭弹窗
|
||
const handleClose = () => {
|
||
resetForm();
|
||
onClose();
|
||
};
|
||
|
||
// 提交表单
|
||
const handleSubmit = async () => {
|
||
// 验证
|
||
if (!formData.name.trim()) {
|
||
toast({
|
||
title: '验证失败',
|
||
description: '请输入表单名称',
|
||
variant: 'destructive'
|
||
});
|
||
return;
|
||
}
|
||
if (!formData.key.trim()) {
|
||
toast({
|
||
title: '验证失败',
|
||
description: '请输入表单标识',
|
||
variant: 'destructive'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 验证表单标识格式(英文字母、数字、中划线)
|
||
if (!/^[a-zA-Z0-9-]+$/.test(formData.key)) {
|
||
toast({
|
||
title: '验证失败',
|
||
description: '表单标识只能包含英文字母、数字和中划线',
|
||
variant: 'destructive'
|
||
});
|
||
return;
|
||
}
|
||
|
||
setSubmitting(true);
|
||
try {
|
||
// 创建表单定义(只保存基本信息,schema 为空)
|
||
const request: FormDefinitionRequest = {
|
||
name: formData.name,
|
||
key: formData.key,
|
||
categoryId: formData.categoryId,
|
||
description: formData.description,
|
||
isTemplate: formData.isTemplate,
|
||
status: 'DRAFT',
|
||
schema: {
|
||
version: '1.0',
|
||
formConfig: {
|
||
labelAlign: 'right',
|
||
size: 'middle'
|
||
},
|
||
fields: []
|
||
}
|
||
};
|
||
|
||
const result = await createDefinition(request);
|
||
|
||
toast({
|
||
title: '创建成功',
|
||
description: '表单基本信息已保存,现在可以开始设计表单'
|
||
});
|
||
|
||
// 跳转到设计器页面
|
||
navigate(`/form/definitions/${result.id}/design`);
|
||
handleClose();
|
||
onSuccess(result.id);
|
||
} catch (error) {
|
||
console.error('创建表单失败:', error);
|
||
toast({
|
||
title: '创建失败',
|
||
description: error instanceof Error ? error.message : '创建表单失败',
|
||
variant: 'destructive'
|
||
});
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Dialog open={visible} onOpenChange={handleClose}>
|
||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>创建表单定义</DialogTitle>
|
||
<DialogDescription>
|
||
第一步:输入表单的基本信息,然后点击"下一步"进入表单设计器
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<Separator />
|
||
|
||
<div className="space-y-6">
|
||
{/* 第一行:表单名称 + 表单标识 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="create-name" className="flex items-center gap-2">
|
||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||
表单名称
|
||
<span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
id="create-name"
|
||
placeholder="例如:员工请假申请表"
|
||
value={formData.name}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||
className="h-10"
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
将显示在表单列表和表单顶部
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="create-key" className="flex items-center gap-2">
|
||
<Tag className="h-4 w-4 text-muted-foreground" />
|
||
表单标识
|
||
<span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
id="create-key"
|
||
placeholder="例如:employee-leave-form"
|
||
value={formData.key}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, key: e.target.value }))}
|
||
className="h-10 font-mono"
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
英文字母、数字和中划线,用于 API 调用
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 第二行:分类 + 设为模板 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="create-category" className="flex items-center gap-2">
|
||
<Folder className="h-4 w-4 text-muted-foreground" />
|
||
表单分类
|
||
</Label>
|
||
<Select
|
||
value={formData.categoryId?.toString() || undefined}
|
||
onValueChange={(value) => setFormData(prev => ({ ...prev, categoryId: Number(value) }))}
|
||
>
|
||
<SelectTrigger id="create-category" className="h-10">
|
||
<SelectValue placeholder="选择表单所属分类" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{categories.map(cat => (
|
||
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||
{cat.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<p className="text-xs text-muted-foreground">
|
||
帮助用户快速找到相关表单
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="create-isTemplate" className="flex items-center gap-2">
|
||
<Copy className="h-4 w-4 text-muted-foreground" />
|
||
设为表单模板
|
||
</Label>
|
||
<div className="flex items-center h-10 rounded-lg border px-4 bg-muted/30">
|
||
<div className="flex-1">
|
||
<span className="text-sm">启用模板功能</span>
|
||
</div>
|
||
<Switch
|
||
id="create-isTemplate"
|
||
checked={formData.isTemplate}
|
||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isTemplate: checked }))}
|
||
/>
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">
|
||
模板表单可以被其他表单引用和复制
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 第三行:描述 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="create-description" className="flex items-center gap-2">
|
||
<AlignLeft className="h-4 w-4 text-muted-foreground" />
|
||
表单描述
|
||
</Label>
|
||
<Textarea
|
||
id="create-description"
|
||
placeholder="简要说明此表单的用途和填写注意事项..."
|
||
value={formData.description}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||
className="min-h-[100px] resize-none"
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
选填,用于帮助用户了解表单用途
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={handleClose} disabled={submitting}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleSubmit} disabled={submitting}>
|
||
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||
下一步:设计表单
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
export default CreateModal;
|
||
|