表单CRUD
This commit is contained in:
parent
d22285bc95
commit
6d8afea807
@ -1,19 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/card';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
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 { FormDesigner } from '@/components/FormDesigner';
|
||||
import type { FormSchema } from '@/components/FormDesigner';
|
||||
import { ArrowLeft, FileText, Tag, Folder, AlignLeft, Info } from 'lucide-react';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { getDefinitionById, createDefinition, updateDefinition } from './service';
|
||||
import { getEnabledCategories } from '../Category/service';
|
||||
import { ArrowLeft, Workflow } from 'lucide-react';
|
||||
import { getDefinitionById, updateDefinition } from './service';
|
||||
import type { FormDefinitionRequest } from './types';
|
||||
import type { FormCategoryResponse } from '../Category/types';
|
||||
|
||||
/**
|
||||
* 表单设计器页面
|
||||
@ -21,79 +14,49 @@ import type { FormCategoryResponse } from '../Category/types';
|
||||
const FormDesignerPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const isEdit = !!id;
|
||||
|
||||
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
|
||||
const [formMeta, setFormMeta] = useState({
|
||||
name: '',
|
||||
key: '',
|
||||
categoryId: undefined as number | undefined,
|
||||
description: '',
|
||||
});
|
||||
const [formSchema, setFormSchema] = useState<FormSchema | null>(null);
|
||||
|
||||
// 加载分类列表
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const result = await getEnabledCategories();
|
||||
setCategories(result || []);
|
||||
} catch (error) {
|
||||
console.error('加载分类失败:', error);
|
||||
}
|
||||
};
|
||||
const [formDefinition, setFormDefinition] = useState<any>(null);
|
||||
|
||||
// 加载表单定义
|
||||
useEffect(() => {
|
||||
loadCategories();
|
||||
if (isEdit && id) {
|
||||
if (id) {
|
||||
loadFormDefinition(Number(id));
|
||||
}
|
||||
}, [id, isEdit]);
|
||||
}, [id]);
|
||||
|
||||
const loadFormDefinition = async (definitionId: number) => {
|
||||
try {
|
||||
const result = await getDefinitionById(definitionId);
|
||||
setFormMeta({
|
||||
name: result.name,
|
||||
key: result.key,
|
||||
categoryId: result.categoryId,
|
||||
description: result.description || '',
|
||||
});
|
||||
setFormDefinition(result);
|
||||
setFormSchema(result.schema);
|
||||
} catch (error) {
|
||||
console.error('加载表单定义失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存表单
|
||||
// 保存表单(只更新 schema,不修改基本信息)
|
||||
const handleSave = async (schema: FormSchema) => {
|
||||
if (!formMeta.name.trim()) {
|
||||
alert('请输入表单名称');
|
||||
return;
|
||||
}
|
||||
if (!formMeta.key.trim()) {
|
||||
alert('请输入表单标识');
|
||||
if (!id || !formDefinition) {
|
||||
alert('表单信息加载失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const request: FormDefinitionRequest = {
|
||||
name: formMeta.name,
|
||||
key: formMeta.key,
|
||||
categoryId: formMeta.categoryId,
|
||||
description: formMeta.description,
|
||||
name: formDefinition.name,
|
||||
key: formDefinition.key,
|
||||
categoryId: formDefinition.categoryId,
|
||||
description: formDefinition.description,
|
||||
isTemplate: formDefinition.isTemplate,
|
||||
schema,
|
||||
status: 'PUBLISHED',
|
||||
status: formDefinition.status || 'DRAFT',
|
||||
};
|
||||
|
||||
try {
|
||||
if (isEdit && id) {
|
||||
await updateDefinition(Number(id), request);
|
||||
} else {
|
||||
await createDefinition(request);
|
||||
}
|
||||
navigate('/form/definitions');
|
||||
alert('保存成功');
|
||||
} catch (error) {
|
||||
console.error('保存表单失败:', error);
|
||||
alert('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
@ -106,147 +69,28 @@ const FormDesignerPage: React.FC = () => {
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10">
|
||||
<Workflow className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
{isEdit ? '编辑表单定义' : '创建表单定义'}
|
||||
{formDefinition?.name || '表单设计'}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{isEdit ? '修改表单的基本信息和字段配置' : '设计您的自定义表单,添加字段并配置验证规则'}
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{formDefinition?.description || '拖拽左侧组件到画布,配置字段属性和验证规则'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" onClick={handleBack}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
返回列表
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 基本信息卡片 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<FileText className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>基本信息</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
设置表单的名称、标识和分类信息
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Separator />
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
{/* 第一行:表单名称 + 表单标识 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
表单名称
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="例如:员工请假申请表"
|
||||
value={formMeta.name}
|
||||
onChange={(e) => setFormMeta(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="key" className="flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-muted-foreground" />
|
||||
表单标识
|
||||
<span className="text-destructive">*</span>
|
||||
{isEdit && <span className="text-xs text-muted-foreground">(不可修改)</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id="key"
|
||||
placeholder="例如:employee-leave-form"
|
||||
value={formMeta.key}
|
||||
onChange={(e) => setFormMeta(prev => ({ ...prev, key: e.target.value }))}
|
||||
disabled={isEdit}
|
||||
className="h-10 font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
英文字母、数字和中划线,用于 API 调用
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第二行:分类 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category" className="flex items-center gap-2">
|
||||
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||
表单分类
|
||||
</Label>
|
||||
<Select
|
||||
value={formMeta.categoryId?.toString() || undefined}
|
||||
onValueChange={(value) => setFormMeta(prev => ({ ...prev, categoryId: Number(value) }))}
|
||||
>
|
||||
<SelectTrigger className="h-10">
|
||||
<SelectValue placeholder="选择表单所属分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(cat => (
|
||||
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Folder className="h-3.5 w-3.5" />
|
||||
{cat.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
帮助用户快速找到相关表单
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 第三行:描述(跨整行) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description" className="flex items-center gap-2">
|
||||
<AlignLeft className="h-4 w-4 text-muted-foreground" />
|
||||
表单描述
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="简要说明此表单的用途和填写注意事项..."
|
||||
value={formMeta.description}
|
||||
onChange={(e) => setFormMeta(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="min-h-[100px] resize-none"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
选填,用于帮助用户了解表单用途
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 表单设计器区域 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10">
|
||||
<Info className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>表单设计</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
拖拽左侧组件到画布,配置字段属性和验证规则
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Separator />
|
||||
<CardContent className="p-0">
|
||||
{/* 表单设计器 */}
|
||||
<div className="rounded-lg">
|
||||
<FormDesigner
|
||||
value={formSchema || undefined}
|
||||
|
||||
283
frontend/src/pages/Form/Definition/components/CreateModal.tsx
Normal file
283
frontend/src/pages/Form/Definition/components/CreateModal.tsx
Normal file
@ -0,0 +1,283 @@
|
||||
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()}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Folder className="h-3.5 w-3.5" />
|
||||
{cat.name}
|
||||
</div>
|
||||
</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;
|
||||
|
||||
@ -0,0 +1,229 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} 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 { Loader2, FileText, Tag, Folder, AlignLeft, Copy } from 'lucide-react';
|
||||
import { getEnabledCategories } from '../../Category/service';
|
||||
import { updateDefinition } from '../service';
|
||||
import type { FormCategoryResponse } from '../../Category/types';
|
||||
import type { FormDefinitionRequest, FormDefinitionResponse } from '../types';
|
||||
|
||||
interface EditBasicInfoModalProps {
|
||||
visible: boolean;
|
||||
record: FormDefinitionResponse | null;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const EditBasicInfoModal: React.FC<EditBasicInfoModalProps> = ({ visible, record, onClose, onSuccess }) => {
|
||||
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(() => {
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const result = await getEnabledCategories();
|
||||
setCategories(result || []);
|
||||
} catch (error) {
|
||||
console.error('加载分类失败:', error);
|
||||
}
|
||||
};
|
||||
if (visible) {
|
||||
loadCategories();
|
||||
if (record) {
|
||||
setFormData({
|
||||
name: record.name,
|
||||
key: record.key,
|
||||
categoryId: record.categoryId,
|
||||
description: record.description || '',
|
||||
isTemplate: record.isTemplate || false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [visible, record]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (!submitting) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!record) return;
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
alert('请输入表单名称');
|
||||
return;
|
||||
}
|
||||
if (!formData.key.trim()) {
|
||||
alert('请输入表单标识');
|
||||
return;
|
||||
}
|
||||
if (!/^[a-zA-Z0-9-]+$/.test(formData.key)) {
|
||||
alert('表单标识只能包含英文字母、数字和中划线');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const request: FormDefinitionRequest = {
|
||||
name: formData.name,
|
||||
key: formData.key,
|
||||
categoryId: formData.categoryId,
|
||||
description: formData.description,
|
||||
schema: record.schema,
|
||||
status: record.status,
|
||||
isTemplate: formData.isTemplate,
|
||||
};
|
||||
|
||||
await updateDefinition(record.id, request);
|
||||
alert('更新成功');
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('更新表单失败:', error);
|
||||
alert('更新表单失败: ' + (error as Error).message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑基本信息</DialogTitle>
|
||||
<DialogDescription>
|
||||
修改表单的名称、标识、分类等基本信息
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-6 py-4">
|
||||
{/* 第一行:表单名称 + 表单标识 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-name" className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
表单名称 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
placeholder="例如:员工请假申请表"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-key" className="flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-muted-foreground" />
|
||||
表单标识 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-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">
|
||||
英文字母、数字和中划线
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第二行:分类 + 设为模板 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-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="edit-category" className="h-10">
|
||||
<SelectValue placeholder="选择表单所属分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(cat => (
|
||||
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Folder className="h-3.5 w-3.5" />
|
||||
{cat.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-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="edit-isTemplate"
|
||||
checked={formData.isTemplate}
|
||||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isTemplate: checked }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第三行:描述 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-description" className="flex items-center gap-2">
|
||||
<AlignLeft className="h-4 w-4 text-muted-foreground" />
|
||||
表单描述
|
||||
</Label>
|
||||
<Textarea
|
||||
id="edit-description"
|
||||
placeholder="简要说明此表单的用途和填写注意事项..."
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="min-h-[100px] resize-none"
|
||||
/>
|
||||
</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 EditBasicInfoModal;
|
||||
|
||||
@ -12,7 +12,7 @@ import { FormRenderer } from '@/components/FormDesigner';
|
||||
import {
|
||||
Loader2, Plus, Search, Eye, Edit, FileText, Ban, Trash2,
|
||||
Database, MoreHorizontal, CheckCircle2, XCircle, Clock, AlertCircle,
|
||||
Folder, Activity
|
||||
Folder, Activity, Settings
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -28,6 +28,8 @@ import type { FormDefinitionResponse, FormDefinitionStatus } from './types';
|
||||
import type { FormCategoryResponse } from '../Category/types';
|
||||
import type { Page } from '@/types/base';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||
import CreateModal from './components/CreateModal';
|
||||
import EditBasicInfoModal from './components/EditBasicInfoModal';
|
||||
|
||||
/**
|
||||
* 表单定义列表页
|
||||
@ -45,6 +47,13 @@ const FormDefinitionList: React.FC = () => {
|
||||
status: undefined as FormDefinitionStatus | undefined,
|
||||
});
|
||||
|
||||
// 创建表单弹窗
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
|
||||
// 编辑基本信息弹窗
|
||||
const [editBasicInfoVisible, setEditBasicInfoVisible] = useState(false);
|
||||
const [editBasicInfoRecord, setEditBasicInfoRecord] = useState<FormDefinitionResponse | null>(null);
|
||||
|
||||
// 预览弹窗
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewForm, setPreviewForm] = useState<FormDefinitionResponse | null>(null);
|
||||
@ -116,12 +125,18 @@ const FormDefinitionList: React.FC = () => {
|
||||
|
||||
// 创建表单
|
||||
const handleCreate = () => {
|
||||
navigate('/form/definitions/create');
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
|
||||
// 编辑表单
|
||||
// 编辑表单设计
|
||||
const handleEdit = (record: FormDefinitionResponse) => {
|
||||
navigate(`/form/definitions/${record.id}/edit`);
|
||||
navigate(`/form/definitions/${record.id}/design`);
|
||||
};
|
||||
|
||||
// 编辑基本信息
|
||||
const handleEditBasicInfo = (record: FormDefinitionResponse) => {
|
||||
setEditBasicInfoRecord(record);
|
||||
setEditBasicInfoVisible(true);
|
||||
};
|
||||
|
||||
// 预览表单
|
||||
@ -427,6 +442,10 @@ const FormDefinitionList: React.FC = () => {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[160px]">
|
||||
<DropdownMenuItem onClick={() => handleEditBasicInfo(record)}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
编辑基本信息
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleViewData(record)}>
|
||||
<Database className="h-4 w-4 mr-2" />
|
||||
查看数据
|
||||
@ -564,6 +583,29 @@ const FormDefinitionList: React.FC = () => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* 创建表单弹窗 */}
|
||||
<CreateModal
|
||||
visible={createModalVisible}
|
||||
onClose={() => setCreateModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
setCreateModalVisible(false);
|
||||
loadData();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 编辑基本信息弹窗 */}
|
||||
<EditBasicInfoModal
|
||||
visible={editBasicInfoVisible}
|
||||
record={editBasicInfoRecord}
|
||||
onClose={() => {
|
||||
setEditBasicInfoVisible(false);
|
||||
setEditBasicInfoRecord(null);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
loadData();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,7 +1,20 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Modal, Form, Input, message, Select} from 'antd';
|
||||
import type {WorkflowDefinition, WorkflowCategory} from '../types';
|
||||
import {saveDefinition, updateDefinition, getWorkflowCategories} from '../service';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} 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 { Loader2, FileText, Tag, AlignLeft, Folder } from 'lucide-react';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import type { WorkflowDefinition, WorkflowCategoryResponse } from '../types';
|
||||
import { saveDefinition, updateDefinition, getWorkflowCategoryList } from '../service';
|
||||
|
||||
interface EditModalProps {
|
||||
visible: boolean;
|
||||
@ -10,181 +23,262 @@ interface EditModalProps {
|
||||
record?: WorkflowDefinition;
|
||||
}
|
||||
|
||||
const EditModal: React.FC<EditModalProps> = ({visible, onClose, onSuccess, record}) => {
|
||||
const [form] = Form.useForm();
|
||||
const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, record }) => {
|
||||
const { toast } = useToast();
|
||||
const isEdit = !!record;
|
||||
const [categories, setCategories] = useState<WorkflowCategory[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState<WorkflowCategory>();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [categories, setCategories] = useState<WorkflowCategoryResponse[]>([]);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
key: '',
|
||||
categoryId: undefined as number | undefined,
|
||||
description: '',
|
||||
triggers: [] as string[],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
loadCategories();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && record) {
|
||||
// 找到当前分类
|
||||
const category = categories.find(c => c.code === record.category);
|
||||
setSelectedCategory(category);
|
||||
|
||||
// 设置表单值,使用lable显示
|
||||
form.setFieldsValue({
|
||||
...record,
|
||||
// 分类显示lable
|
||||
category: {
|
||||
label: category?.lable,
|
||||
value: record.category
|
||||
},
|
||||
// 触发器显示lable
|
||||
triggers: record.triggers?.map(triggerCode => ({
|
||||
label: category?.supportedTriggers.find(t => t.code === triggerCode)?.lable,
|
||||
value: triggerCode
|
||||
})) || []
|
||||
if (record) {
|
||||
setFormData({
|
||||
name: record.name,
|
||||
key: record.key,
|
||||
categoryId: record.categoryId,
|
||||
description: record.description || '',
|
||||
triggers: record.triggers || [],
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
name: '',
|
||||
key: '',
|
||||
categoryId: undefined,
|
||||
description: '',
|
||||
triggers: [],
|
||||
});
|
||||
}
|
||||
}, [visible, record, categories]);
|
||||
}
|
||||
}, [visible, record]);
|
||||
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const data = await getWorkflowCategories();
|
||||
const data = await getWorkflowCategoryList();
|
||||
setCategories(data);
|
||||
} catch (error) {
|
||||
console.error('加载工作流分类失败:', error);
|
||||
message.error('加载工作流分类失败');
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: '加载工作流分类失败',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCategoryChange = (selected: { value: string, label: string }) => {
|
||||
const category = categories.find(c => c.code === selected.value);
|
||||
setSelectedCategory(category);
|
||||
// 当切换分类时,清空触发器选择
|
||||
form.setFieldValue('triggers', []);
|
||||
const handleClose = () => {
|
||||
if (!submitting) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOk = async () => {
|
||||
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-Z_][a-zA-Z0-9_-]*$/.test(formData.key)) {
|
||||
toast({
|
||||
title: '验证失败',
|
||||
description: '流程标识只能包含字母、数字、下划线和连字符,且必须以字母或下划线开头',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (/^xml/i.test(formData.key)) {
|
||||
toast({
|
||||
title: '验证失败',
|
||||
description: '流程标识不能以xml开头',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!formData.categoryId) {
|
||||
toast({
|
||||
title: '验证失败',
|
||||
description: '请选择流程分类',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const submitData = {
|
||||
...values,
|
||||
// 提取code值提交给后端
|
||||
category: values.category.value,
|
||||
triggers: values.triggers.map((t: {value: string}) => t.value),
|
||||
const submitData: WorkflowDefinition = {
|
||||
...formData,
|
||||
id: record?.id || 0,
|
||||
flowVersion: isEdit ? record.flowVersion : 1,
|
||||
status: isEdit ? record.status : 'DRAFT'
|
||||
};
|
||||
status: isEdit ? record.status : 'DRAFT',
|
||||
graph: record?.graph || { nodes: [], edges: [] },
|
||||
formConfig: record?.formConfig || { formItems: [] },
|
||||
} as WorkflowDefinition;
|
||||
|
||||
if (isEdit) {
|
||||
await updateDefinition(record.id, {
|
||||
...record,
|
||||
...submitData,
|
||||
if (isEdit && record) {
|
||||
await updateDefinition(record.id, submitData);
|
||||
toast({
|
||||
title: '更新成功',
|
||||
description: `工作流 "${formData.name}" 已更新`,
|
||||
});
|
||||
} else {
|
||||
await saveDefinition(submitData as WorkflowDefinition);
|
||||
await saveDefinition(submitData);
|
||||
toast({
|
||||
title: '创建成功',
|
||||
description: `工作流 "${formData.name}" 已创建`,
|
||||
});
|
||||
}
|
||||
message.success(isEdit ? '更新成功' : '保存成功');
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
toast({
|
||||
title: isEdit ? '更新失败' : '创建失败',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedCategory = categories.find(c => c.id === formData.categoryId);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={isEdit ? '编辑流程' : '新建流程'}
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={() => {
|
||||
onClose();
|
||||
form.resetFields();
|
||||
<Dialog open={visible} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? '编辑流程' : '新建流程'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-6 py-4">
|
||||
{/* 流程分类 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="categoryId" className="flex items-center gap-2">
|
||||
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||
流程分类 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.categoryId?.toString() || undefined}
|
||||
onValueChange={(value) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
categoryId: Number(value),
|
||||
triggers: [], // 切换分类时清空触发器
|
||||
}));
|
||||
}}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
preserve={false}
|
||||
>
|
||||
<Form.Item
|
||||
name="category"
|
||||
label="流程分类"
|
||||
rules={[{required: true, message: '请选择流程分类'}]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择流程分类"
|
||||
onChange={handleCategoryChange}
|
||||
disabled={isEdit}
|
||||
labelInValue
|
||||
>
|
||||
{(categories || []).map(category => (
|
||||
<Select.Option key={category.code} value={category.code}>
|
||||
{category.lable}
|
||||
</Select.Option>
|
||||
<SelectTrigger id="categoryId" className="h-10">
|
||||
<SelectValue placeholder="请选择流程分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(cat => (
|
||||
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||
{cat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="triggers"
|
||||
label="触发方式"
|
||||
rules={[{required: true, message: '请选择触发方式'}]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="请选择触发方式"
|
||||
disabled={!selectedCategory || isEdit}
|
||||
labelInValue
|
||||
>
|
||||
{(selectedCategory?.supportedTriggers || []).map(trigger => (
|
||||
<Select.Option key={trigger.code} value={trigger.code}>
|
||||
{trigger.lable}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="流程名称"
|
||||
rules={[{required: true, message: '请输入流程名称'}]}
|
||||
>
|
||||
<Input placeholder="请输入流程名称"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label="流程标识"
|
||||
rules={[
|
||||
{ required: true, message: '请输入流程标识' },
|
||||
{
|
||||
pattern: /^[a-zA-Z_][a-zA-Z0-9_-]*$/,
|
||||
message: '流程标识只能包含字母、数字、下划线(_)和连字符(-),且必须以字母或下划线开头'
|
||||
},
|
||||
{
|
||||
pattern: /^(?!xml)/i,
|
||||
message: '流程标识不能以xml开头(不区分大小写)'
|
||||
},
|
||||
{
|
||||
pattern: /^[^.\s]*$/,
|
||||
message: '流程标识不能包含空格和点号'
|
||||
},
|
||||
{
|
||||
max: 64,
|
||||
message: '流程标识长度不能超过64个字符'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入流程标识,建议使用小写字母和下划线" disabled={isEdit}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述"
|
||||
>
|
||||
<Input.TextArea placeholder="请输入流程描述"/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
{isEdit && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
编辑时不可修改分类
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 流程名称 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
流程名称 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="例如:Jenkins 构建流程"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 流程标识 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="key" className="flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-muted-foreground" />
|
||||
流程标识 <span className="text-destructive">*</span>
|
||||
{isEdit && <span className="text-xs text-muted-foreground">(不可修改)</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id="key"
|
||||
placeholder="例如:jenkins_build_workflow"
|
||||
value={formData.key}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, key: e.target.value }))}
|
||||
disabled={isEdit}
|
||||
className="h-10 font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
只能包含字母、数字、下划线和连字符,建议使用小写字母和下划线
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 描述 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description" className="flex items-center gap-2">
|
||||
<AlignLeft className="h-4 w-4 text-muted-foreground" />
|
||||
流程描述
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="简要说明此工作流的用途..."
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="min-h-[100px] resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 提示:触发方式在设计阶段配置 */}
|
||||
{selectedCategory && selectedCategory.supportedTriggers && selectedCategory.supportedTriggers.length > 0 && (
|
||||
<div className="rounded-lg border p-4 bg-muted/30">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<strong>支持的触发方式:</strong> {selectedCategory.supportedTriggers.join(', ')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
创建后可在工作流设计页面配置具体的触发条件
|
||||
</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" />}
|
||||
{isEdit ? '更新' : '创建'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,16 +1,33 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {Table, Card, Button, Space, Tag, message, Modal} from 'antd';
|
||||
import {PlusOutlined, ExclamationCircleOutlined} from '@ant-design/icons';
|
||||
import {useNavigate} from 'react-router-dom';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import {
|
||||
Loader2, Plus, Search, Edit, Trash2, Play, CheckCircle2,
|
||||
Clock, Activity, Workflow, MoreHorizontal, AlertCircle, Eye
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import * as service from './service';
|
||||
import type {WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategory} from './types';
|
||||
import {DEFAULT_PAGE_SIZE, DEFAULT_CURRENT} from '@/utils/page';
|
||||
import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategoryResponse } from './types';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||
import EditModal from './components/EditModal';
|
||||
|
||||
const {confirm} = Modal;
|
||||
|
||||
const WorkflowDefinitionList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pageData, setPageData] = useState<{
|
||||
content: WorkflowDefinition[];
|
||||
@ -20,10 +37,17 @@ const WorkflowDefinitionList: React.FC = () => {
|
||||
} | null>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRecord, setCurrentRecord] = useState<WorkflowDefinition>();
|
||||
const [categories, setCategories] = useState<WorkflowCategory[]>([]);
|
||||
const [categories, setCategories] = useState<WorkflowCategoryResponse[]>([]);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteRecord, setDeleteRecord] = useState<WorkflowDefinition | null>(null);
|
||||
const [deployDialogOpen, setDeployDialogOpen] = useState(false);
|
||||
const [deployRecord, setDeployRecord] = useState<WorkflowDefinition | null>(null);
|
||||
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
|
||||
pageNum: DEFAULT_CURRENT - 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
name: '',
|
||||
categoryId: undefined,
|
||||
status: undefined
|
||||
});
|
||||
|
||||
const loadData = async (params: WorkflowDefinitionQuery) => {
|
||||
@ -33,7 +57,11 @@ const WorkflowDefinitionList: React.FC = () => {
|
||||
setPageData(data);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: error.message,
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -42,17 +70,19 @@ const WorkflowDefinitionList: React.FC = () => {
|
||||
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const data = await service.getWorkflowCategories();
|
||||
const data = await service.getWorkflowCategoryList();
|
||||
setCategories(data);
|
||||
} catch (error) {
|
||||
console.error('加载工作流分类失败:', error);
|
||||
message.error('加载工作流分类失败');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData(query);
|
||||
loadCategories();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData(query);
|
||||
}, [query]);
|
||||
|
||||
const handleCreateFlow = () => {
|
||||
@ -74,169 +104,414 @@ const WorkflowDefinitionList: React.FC = () => {
|
||||
setCurrentRecord(undefined);
|
||||
};
|
||||
|
||||
const handleDeploy = async (id: number) => {
|
||||
confirm({
|
||||
title: '确认发布',
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
content: '确定要发布该流程定义吗?发布后将不能修改。',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await service.publishDefinition(id);
|
||||
message.success('发布成功');
|
||||
loadData(query);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
const handleSearch = () => {
|
||||
setQuery(prev => ({
|
||||
...prev,
|
||||
pageNum: 0,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setQuery({
|
||||
pageNum: 0,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
name: '',
|
||||
categoryId: undefined,
|
||||
status: undefined
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
confirm({
|
||||
title: '确认删除',
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
content: '确定要删除该流程定义吗?删除后不可恢复。',
|
||||
onOk: async () => {
|
||||
const handleDeploy = (record: WorkflowDefinition) => {
|
||||
setDeployRecord(record);
|
||||
setDeployDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDeploy = async () => {
|
||||
if (!deployRecord) return;
|
||||
try {
|
||||
await service.deleteDefinition(id);
|
||||
message.success('删除成功');
|
||||
await service.publishDefinition(deployRecord.id);
|
||||
toast({
|
||||
title: '发布成功',
|
||||
description: `工作流 "${deployRecord.name}" 已发布`,
|
||||
});
|
||||
loadData(query);
|
||||
setDeployDialogOpen(false);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
toast({
|
||||
title: '发布失败',
|
||||
description: error.message,
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (record: WorkflowDefinition) => {
|
||||
setDeleteRecord(record);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!deleteRecord) return;
|
||||
try {
|
||||
await service.deleteDefinition(deleteRecord.id);
|
||||
toast({
|
||||
title: '删除成功',
|
||||
description: `工作流 "${deleteRecord.name}" 已删除`,
|
||||
});
|
||||
loadData(query);
|
||||
setDeleteDialogOpen(false);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast({
|
||||
title: '删除失败',
|
||||
description: error.message,
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartFlow = async (record: WorkflowDefinition) => {
|
||||
try {
|
||||
await service.startWorkflowInstance(record.key, record.category);
|
||||
message.success('流程启动成功');
|
||||
await service.startWorkflowInstance(record.key, record.categoryId);
|
||||
toast({
|
||||
title: '启动成功',
|
||||
description: `工作流 "${record.name}" 已启动`,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
toast({
|
||||
title: '启动失败',
|
||||
description: error.message,
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '流程名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '流程标识',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
},
|
||||
{
|
||||
title: '流程分类',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
render: (category: string) => {
|
||||
const categoryInfo = categories.find(c => c.code === category);
|
||||
return categoryInfo?.label || category;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '触发方式',
|
||||
dataIndex: 'triggers',
|
||||
key: 'triggers',
|
||||
render: (triggers: string[], record: WorkflowDefinition) => {
|
||||
const categoryInfo = categories.find(c => c.code === record.category);
|
||||
return (triggers || [])?.map(triggerCode => {
|
||||
const triggerInfo = categoryInfo?.supportedTriggers?.find(t => t.code === triggerCode);
|
||||
// 状态徽章
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusMap: Record<string, {
|
||||
variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline';
|
||||
text: string;
|
||||
icon: React.ElementType
|
||||
}> = {
|
||||
DRAFT: { variant: 'outline', text: '草稿', icon: Clock },
|
||||
PUBLISHED: { variant: 'success', text: '已发布', icon: CheckCircle2 },
|
||||
};
|
||||
const statusInfo = statusMap[status] || { variant: 'outline', text: status, icon: Clock };
|
||||
const Icon = statusInfo.icon;
|
||||
return (
|
||||
<Tag key={triggerCode}>
|
||||
{triggerInfo?.lable || triggerCode}
|
||||
</Tag>
|
||||
<Badge variant={statusInfo.variant} className="flex items-center gap-1">
|
||||
<Icon className="h-3 w-3" />
|
||||
{statusInfo.text}
|
||||
</Badge>
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '版本',
|
||||
dataIndex: 'flowVersion',
|
||||
key: 'flowVersion',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag color={status === 'DRAFT' ? 'orange' : 'green'}>
|
||||
{status === 'DRAFT' ? '草稿' : '已发布'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
render: (_: any, record: WorkflowDefinition) => (
|
||||
<Space size="middle">
|
||||
{record.status === 'DRAFT' && (
|
||||
<>
|
||||
<a onClick={() => handleEditFlow(record)}>编辑</a>
|
||||
<a onClick={() => handleDesignFlow(record)}>设计</a>
|
||||
</>
|
||||
)}
|
||||
{record.status === 'DRAFT' && (
|
||||
<a onClick={() => handleDeploy(record.id)}>发布</a>
|
||||
)}
|
||||
<a onClick={() => handleDelete(record.id)}>删除</a>
|
||||
{record.status !== 'DRAFT' && (
|
||||
<a onClick={() => handleStartFlow(record)}>启动</a>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// 统计数据
|
||||
const stats = useMemo(() => {
|
||||
const total = pageData?.totalElements || 0;
|
||||
const draftCount = pageData?.content?.filter(d => d.status === 'DRAFT').length || 0;
|
||||
const publishedCount = pageData?.content?.filter(d => d.status !== 'DRAFT').length || 0;
|
||||
return { total, draftCount, publishedCount };
|
||||
}, [pageData]);
|
||||
|
||||
const pageCount = pageData?.totalElements ? Math.ceil(pageData.totalElements / (query.pageSize || DEFAULT_PAGE_SIZE)) : 0;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<Button type="primary" icon={<PlusOutlined/>} onClick={handleCreateFlow}>
|
||||
新建流程
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-foreground">工作流定义管理</h1>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
<Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-blue-700">总工作流</CardTitle>
|
||||
<Activity className="h-4 w-4 text-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.total}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">全部工作流定义</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-yellow-500/10 to-yellow-500/5 border-yellow-500/20">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-yellow-700">草稿</CardTitle>
|
||||
<Clock className="h-4 w-4 text-yellow-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.draftCount}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">待发布的工作流</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-green-700">已发布</CardTitle>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.publishedCount}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">正在使用的工作流</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>工作流列表</CardTitle>
|
||||
<Button onClick={handleCreateFlow}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建工作流
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={pageData?.content}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
scroll={{ x: 1300 }}
|
||||
pagination={{
|
||||
current: (query.pageNum || 0) + 1,
|
||||
pageSize: query.pageSize,
|
||||
total: pageData?.totalElements || 0,
|
||||
onChange: (page, pageSize) => setQuery({
|
||||
...query,
|
||||
pageNum: page - 1,
|
||||
pageSize
|
||||
}),
|
||||
}}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* 搜索栏 */}
|
||||
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||
<div className="flex-1 max-w-md">
|
||||
<Input
|
||||
placeholder="搜索工作流名称"
|
||||
value={query.name}
|
||||
onChange={(e) => setQuery(prev => ({ ...prev, name: e.target.value }))}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
value={query.categoryId?.toString() || undefined}
|
||||
onValueChange={(value) => setQuery(prev => ({ ...prev, categoryId: Number(value) }))}
|
||||
>
|
||||
<SelectTrigger className="w-[160px] h-9">
|
||||
<SelectValue placeholder="全部分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(cat => (
|
||||
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||
{cat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={query.status || undefined}
|
||||
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value }))}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] h-9">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="DRAFT">草稿</SelectItem>
|
||||
<SelectItem value="PUBLISHED">已发布</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={handleSearch} className="h-9">
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
搜索
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleReset} className="h-9">
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 表格 */}
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">流程名称</TableHead>
|
||||
<TableHead className="w-[150px]">流程标识</TableHead>
|
||||
<TableHead className="w-[120px]">流程分类</TableHead>
|
||||
<TableHead className="w-[80px]">版本</TableHead>
|
||||
<TableHead className="w-[100px]">状态</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="h-24 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
<span className="text-sm text-muted-foreground">加载中...</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : pageData?.content && pageData.content.length > 0 ? (
|
||||
pageData.content.map((record) => {
|
||||
const categoryInfo = categories.find(c => c.id === record.categoryId);
|
||||
return (
|
||||
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||
<TableCell className="font-medium">{record.name}</TableCell>
|
||||
<TableCell>
|
||||
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
|
||||
{record.key}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{categoryInfo ? (
|
||||
<Badge variant="outline">
|
||||
{categoryInfo.name}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">未分类</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="font-mono text-sm">{record.flowVersion || 1}</span>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(record.status || 'DRAFT')}</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm line-clamp-1">{record.description || '-'}</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">打开菜单</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{record.status === 'DRAFT' && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={() => handleEditFlow(record)}>
|
||||
<Edit className="h-4 w-4 mr-2" /> 编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDesignFlow(record)}>
|
||||
<Workflow className="h-4 w-4 mr-2" /> 设计
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDeploy(record)}>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" /> 发布
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{record.status !== 'DRAFT' && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={() => handleStartFlow(record)}>
|
||||
<Play className="h-4 w-4 mr-2" /> 启动
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDesignFlow(record)}>
|
||||
<Eye className="h-4 w-4 mr-2" /> 查看
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(record)}
|
||||
className="text-red-600 focus:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" /> 删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="h-24 text-center">
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<Workflow className="w-16 h-16 mb-4 text-muted-foreground/50" />
|
||||
<div className="text-lg font-semibold mb-2">暂无工作流定义</div>
|
||||
<div className="text-sm">点击右上角"新建工作流"开始设计您的第一个工作流。</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{pageCount > 1 && (
|
||||
<DataTablePagination
|
||||
pageIndex={(query.pageNum || 0) + 1}
|
||||
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
|
||||
pageCount={pageCount}
|
||||
onPageChange={(page) => setQuery(prev => ({
|
||||
...prev,
|
||||
pageNum: page - 1
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* EditModal */}
|
||||
<EditModal
|
||||
visible={modalVisible}
|
||||
onClose={handleModalClose}
|
||||
onSuccess={() => loadData(query)}
|
||||
record={currentRecord}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 发布确认对话框 */}
|
||||
{deployRecord && (
|
||||
<Dialog open={deployDialogOpen} onOpenChange={setDeployDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-green-600">
|
||||
<CheckCircle2 className="h-5 w-5" /> 确认发布工作流?
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
您确定要发布工作流 "<span className="font-semibold text-foreground">{deployRecord.name}</span>" 吗?
|
||||
发布后将可以启动执行。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeployDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={confirmDeploy}>
|
||||
确认发布
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
{deleteRecord && (
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-red-600">
|
||||
<AlertCircle className="h-5 w-5" /> 确认删除工作流?
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
您确定要删除工作流 "<span className="font-semibold text-foreground">{deleteRecord.name}</span>" 吗?
|
||||
此操作不可逆。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<span className="font-medium">标识:</span> <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">{deleteRecord.key}</code>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">版本:</span> <span className="font-mono text-sm">{deleteRecord.flowVersion || 1}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">状态:</span> {getStatusBadge(deleteRecord.status || 'DRAFT')}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={confirmDelete}>
|
||||
确认删除
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import request from '@/utils/request';
|
||||
import {WorkflowDefinition, WorkflowDefinitionQuery} from './types';
|
||||
import {
|
||||
WorkflowDefinition,
|
||||
WorkflowDefinitionQuery,
|
||||
WorkflowCategoryResponse,
|
||||
WorkflowCategoryQuery,
|
||||
WorkflowCategoryRequest
|
||||
} from './types';
|
||||
import {Page} from '@/types/base';
|
||||
import {WorkflowCategory} from './types'; // Add this line
|
||||
|
||||
const DEFINITION_URL = '/api/v1/workflow/definition';
|
||||
const INSTANCE_URL = '/api/v1/workflow/instance';
|
||||
const CATEGORY_URL = '/api/v1/workflow/categories';
|
||||
|
||||
export const getDefinitions = (params?: WorkflowDefinitionQuery) =>
|
||||
request.get<Page<WorkflowDefinition>>(`${DEFINITION_URL}/page`, {params});
|
||||
@ -36,21 +42,62 @@ export const publishDefinition = (id: number) =>
|
||||
request.post<void>(`${DEFINITION_URL}/${id}/published`);
|
||||
|
||||
/**
|
||||
* 获取工作流分类列表
|
||||
* @returns Promise<WorkflowCategory[]> 工作流分类列表
|
||||
* 分页查询工作流分类
|
||||
* @param params 查询参数
|
||||
* @returns Promise<Page<WorkflowCategoryResponse>>
|
||||
*/
|
||||
export const getWorkflowCategories = () =>
|
||||
request.get<WorkflowCategory[]>(`${DEFINITION_URL}/categories`);
|
||||
export const getWorkflowCategories = (params?: WorkflowCategoryQuery) =>
|
||||
request.get<Page<WorkflowCategoryResponse>>(`${CATEGORY_URL}/page`, {params});
|
||||
|
||||
/**
|
||||
* 查询工作流分类列表
|
||||
* @returns Promise<WorkflowCategoryResponse[]>
|
||||
*/
|
||||
export const getWorkflowCategoryList = () =>
|
||||
request.get<WorkflowCategoryResponse[]>(`${CATEGORY_URL}/list`);
|
||||
|
||||
/**
|
||||
* 获取单个工作流分类
|
||||
* @param id 分类ID
|
||||
* @returns Promise<WorkflowCategoryResponse>
|
||||
*/
|
||||
export const getWorkflowCategoryById = (id: number) =>
|
||||
request.get<WorkflowCategoryResponse>(`${CATEGORY_URL}/${id}`);
|
||||
|
||||
/**
|
||||
* 创建工作流分类
|
||||
* @param data 分类数据
|
||||
* @returns Promise<WorkflowCategoryResponse>
|
||||
*/
|
||||
export const createWorkflowCategory = (data: WorkflowCategoryRequest) =>
|
||||
request.post<WorkflowCategoryResponse>(CATEGORY_URL, data);
|
||||
|
||||
/**
|
||||
* 更新工作流分类
|
||||
* @param id 分类ID
|
||||
* @param data 分类数据
|
||||
* @returns Promise<WorkflowCategoryResponse>
|
||||
*/
|
||||
export const updateWorkflowCategory = (id: number, data: WorkflowCategoryRequest) =>
|
||||
request.put<WorkflowCategoryResponse>(`${CATEGORY_URL}/${id}`, data);
|
||||
|
||||
/**
|
||||
* 删除工作流分类
|
||||
* @param id 分类ID
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export const deleteWorkflowCategory = (id: number) =>
|
||||
request.delete<void>(`${CATEGORY_URL}/${id}`);
|
||||
|
||||
/**
|
||||
* 启动工作流实例
|
||||
* @param processKey 流程定义key
|
||||
* @param categoryCode 分类编码
|
||||
* @param categoryId 分类ID
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export const startWorkflowInstance = (processKey: string, categoryCode: string) =>
|
||||
export const startWorkflowInstance = (processKey: string, categoryId?: number) =>
|
||||
request.post<void>(`${INSTANCE_URL}/start`, {
|
||||
processKey,
|
||||
businessKey: `${categoryCode}_${Date.now()}`,
|
||||
businessKey: `workflow_${Date.now()}`,
|
||||
categoryId
|
||||
});
|
||||
@ -7,7 +7,7 @@ export interface WorkflowDefinition extends BaseResponse {
|
||||
description?: string;
|
||||
flowVersion?: number;
|
||||
status?: string;
|
||||
category: string;
|
||||
categoryId?: number; // 分类ID
|
||||
triggers: string[];
|
||||
graph: {
|
||||
nodes: WorkflowDefinitionNode[];
|
||||
@ -60,23 +60,46 @@ export interface WorkflowDefinitionNode {
|
||||
export interface WorkflowDefinitionQuery extends BaseQuery {
|
||||
name?: string;
|
||||
key?: string;
|
||||
categoryId?: number; // 分类ID筛选
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流触发器类型
|
||||
* 工作流分类响应
|
||||
*/
|
||||
export interface WorkflowTrigger {
|
||||
export interface WorkflowCategoryResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
sort: number;
|
||||
supportedTriggers?: string[]; // 触发方式代码列表,如 ["MANUAL","SCHEDULED"]
|
||||
enabled: boolean;
|
||||
createBy?: string;
|
||||
createTime?: string;
|
||||
updateBy?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流分类
|
||||
* 工作流分类查询参数
|
||||
*/
|
||||
export interface WorkflowCategory {
|
||||
code: string;
|
||||
label: string;
|
||||
description: string;
|
||||
supportedTriggers: WorkflowTrigger[];
|
||||
export interface WorkflowCategoryQuery extends BaseQuery {
|
||||
name?: string;
|
||||
code?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流分类创建/更新请求
|
||||
*/
|
||||
export interface WorkflowCategoryRequest {
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
sort?: number;
|
||||
supportedTriggers?: string[];
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ const router = createBrowserRouter([
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'definitions/:id/edit',
|
||||
path: 'definitions/:id/design',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<FormDefinitionDesigner/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user