增加审批组件

This commit is contained in:
dengqichen 2025-10-25 16:11:40 +08:00
parent 4599367d33
commit 0a64cfc339
8 changed files with 223 additions and 376 deletions

View File

@ -38,7 +38,7 @@ const FormDataDetail: React.FC = () => {
// 返回列表
const handleBack = () => {
navigate('/form/data');
navigate('/workflow/form/data');
};
// 状态徽章

View File

@ -97,7 +97,7 @@ const FormDataList: React.FC = () => {
// 查看详情
const handleView = (record: FormDataResponse) => {
navigate(`/form/data/${record.id}`);
navigate(`/workflow/form/data/${record.id}`);
};
// 删除

View File

@ -76,7 +76,7 @@ const FormDesignerPage: React.FC = () => {
// 返回列表
const handleBack = () => {
navigate('/form/definitions');
navigate('/workflow/form');
};
return (

View File

@ -1,248 +0,0 @@
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 { useToast } from '@/components/ui/use-toast';
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 { 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(() => {
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()) {
toast({
variant: "destructive",
title: "验证失败",
description: "请输入表单名称",
});
return;
}
if (!formData.key.trim()) {
toast({
variant: "destructive",
title: "验证失败",
description: "请输入表单标识",
});
return;
}
if (!/^[a-zA-Z0-9-]+$/.test(formData.key)) {
toast({
variant: "destructive",
title: "验证失败",
description: "表单标识只能包含英文字母、数字和中划线",
});
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,
version: record.version, // 乐观锁版本号
};
await updateDefinition(record.id, request);
toast({
title: "更新成功",
description: `表单 "${formData.name}" 的基本信息已更新`,
});
onSuccess();
onClose();
} catch (error) {
console.error('更新表单失败:', error);
toast({
variant: "destructive",
title: "更新失败",
description: error instanceof Error ? 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()}>
{cat.name}
</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;

View File

@ -10,21 +10,29 @@ 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 { createDefinition, updateDefinition } from '../service';
import { getEnabledCategories } from '../../Category/service';
import type { FormDefinitionRequest } from '../types';
import type { FormDefinitionRequest, FormDefinitionResponse } from '../types';
import type { FormCategoryResponse } from '../../Category/types';
interface CreateModalProps {
interface FormBasicInfoModalProps {
mode: 'create' | 'edit';
visible: boolean;
record?: FormDefinitionResponse | null;
onClose: () => void;
onSuccess: (id: number) => void;
onSuccess: (id?: number) => void;
}
/**
*
* /
*/
const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }) => {
const FormBasicInfoModal: React.FC<FormBasicInfoModalProps> = ({
mode,
visible,
record,
onClose,
onSuccess
}) => {
const navigate = useNavigate();
const { toast } = useToast();
const [submitting, setSubmitting] = useState(false);
@ -37,12 +45,26 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
isTemplate: false,
});
// 加载分类列表
// 加载分类列表和初始化数据
useEffect(() => {
if (visible) {
loadCategories();
if (mode === 'edit' && record) {
setFormData({
name: record.name,
key: record.key,
categoryId: record.categoryId,
description: record.description || '',
isTemplate: record.isTemplate || false,
});
} else if (mode === 'create') {
resetForm();
}
} else {
// 弹窗关闭时重置表单数据
resetForm();
}
}, [visible]);
}, [visible, mode, record?.id]); // 只依赖 record.id 而不是整个 record 对象
const loadCategories = async () => {
try {
@ -65,9 +87,11 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
};
// 关闭弹窗
const handleClose = () => {
resetForm();
onClose();
const handleClose = (open: boolean) => {
// 当 Dialog 的 open 状态变为 false 时才执行关闭逻辑
if (!open && !submitting) {
onClose();
}
};
// 提交表单
@ -102,54 +126,81 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
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: []
}
};
if (mode === 'create') {
// 创建模式
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);
const result = await createDefinition(request);
toast({
title: '创建成功',
description: '表单基本信息已保存,现在可以开始设计表单'
});
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'
});
// 跳转到设计器页面
navigate(`/workflow/form/${result.id}/design`);
onClose();
onSuccess(result.id);
} else {
// 编辑模式
if (!record) return;
const request: FormDefinitionRequest = {
name: formData.name,
key: formData.key,
categoryId: formData.categoryId,
description: formData.description,
schema: record.schema,
status: record.status,
isTemplate: formData.isTemplate,
version: record.version, // 乐观锁版本号
};
await updateDefinition(record.id, request);
toast({
title: "更新成功",
description: `表单 "${formData.name}" 的基本信息已更新`,
});
// 只调用 onSuccess让父组件管理关闭逻辑
onSuccess();
}
} catch (error: any) {
console.error(`${mode === 'create' ? '创建' : '更新'}表单失败:`, error);
// 错误提示已在 request.ts 中统一处理,这里只做日志记录和状态重置
} finally {
setSubmitting(false);
}
};
// 根据模式调整文案
const title = mode === 'create' ? '创建表单定义' : '编辑基本信息';
const description = mode === 'create'
? '第一步:输入表单的基本信息,然后点击"下一步"进入表单设计器'
: '修改表单的名称、标识、分类等基本信息';
const submitButtonText = mode === 'create' ? '下一步:设计表单' : '保存更改';
return (
<Dialog open={visible} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
"下一步"
{description}
</DialogDescription>
</DialogHeader>
@ -159,13 +210,13 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
{/* 第一行:表单名称 + 表单标识 */}
<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">
<Label htmlFor="form-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"
id="form-name"
placeholder="例如:员工请假申请表"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
@ -177,13 +228,13 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
</div>
<div className="space-y-2">
<Label htmlFor="create-key" className="flex items-center gap-2">
<Label htmlFor="form-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"
id="form-key"
placeholder="例如employee-leave-form"
value={formData.key}
onChange={(e) => setFormData(prev => ({ ...prev, key: e.target.value }))}
@ -198,7 +249,7 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
{/* 第二行:分类 + 设为模板 */}
<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">
<Label htmlFor="form-category" className="flex items-center gap-2">
<Folder className="h-4 w-4 text-muted-foreground" />
</Label>
@ -206,7 +257,7 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
value={formData.categoryId?.toString() || undefined}
onValueChange={(value) => setFormData(prev => ({ ...prev, categoryId: Number(value) }))}
>
<SelectTrigger id="create-category" className="h-10">
<SelectTrigger id="form-category" className="h-10">
<SelectValue placeholder="选择表单所属分类" />
</SelectTrigger>
<SelectContent>
@ -223,7 +274,7 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
</div>
<div className="space-y-2">
<Label htmlFor="create-isTemplate" className="flex items-center gap-2">
<Label htmlFor="form-isTemplate" className="flex items-center gap-2">
<Copy className="h-4 w-4 text-muted-foreground" />
</Label>
@ -232,7 +283,7 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
<span className="text-sm"></span>
</div>
<Switch
id="create-isTemplate"
id="form-isTemplate"
checked={formData.isTemplate}
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isTemplate: checked }))}
/>
@ -245,12 +296,12 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
{/* 第三行:描述 */}
<div className="space-y-2">
<Label htmlFor="create-description" className="flex items-center gap-2">
<Label htmlFor="form-description" className="flex items-center gap-2">
<AlignLeft className="h-4 w-4 text-muted-foreground" />
</Label>
<Textarea
id="create-description"
id="form-description"
placeholder="简要说明此表单的用途和填写注意事项..."
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
@ -263,12 +314,12 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
</div>
<DialogFooter>
<Button variant="outline" onClick={handleClose} disabled={submitting}>
<Button variant="outline" onClick={() => handleClose(false)} disabled={submitting}>
</Button>
<Button onClick={handleSubmit} disabled={submitting}>
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{submitButtonText}
</Button>
</DialogFooter>
</DialogContent>
@ -276,5 +327,5 @@ const CreateModal: React.FC<CreateModalProps> = ({ visible, onClose, onSuccess }
);
};
export default CreateModal;
export default FormBasicInfoModal;

View File

@ -29,8 +29,7 @@ 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';
import FormBasicInfoModal from './components/FormBasicInfoModal';
import CategoryManageDialog from './components/CategoryManageDialog';
/**
@ -136,7 +135,7 @@ const FormDefinitionList: React.FC = () => {
// 编辑表单设计
const handleEdit = (record: FormDefinitionResponse) => {
navigate(`/form/definitions/${record.id}/design`);
navigate(`/workflow/form/${record.id}/design`);
};
// 编辑基本信息
@ -195,7 +194,7 @@ const FormDefinitionList: React.FC = () => {
// 查看数据
const handleViewData = (record: FormDefinitionResponse) => {
navigate(`/form/data?formDefinitionId=${record.id}`);
navigate(`/workflow/form/data?formDefinitionId=${record.id}`);
};
// 根据分类 ID 获取分类信息
@ -601,7 +600,8 @@ const FormDefinitionList: React.FC = () => {
)}
{/* 创建表单弹窗 */}
<CreateModal
<FormBasicInfoModal
mode="create"
visible={createModalVisible}
onClose={() => setCreateModalVisible(false)}
onSuccess={() => {
@ -611,15 +611,26 @@ const FormDefinitionList: React.FC = () => {
/>
{/* 编辑基本信息弹窗 */}
<EditBasicInfoModal
<FormBasicInfoModal
mode="edit"
visible={editBasicInfoVisible}
record={editBasicInfoRecord}
onClose={() => {
// 先关闭弹窗
setEditBasicInfoVisible(false);
setEditBasicInfoRecord(null);
// 延迟清空 record等待 Dialog 关闭动画完成(避免遮罩层残留)
setTimeout(() => {
setEditBasicInfoRecord(null);
}, 300);
}}
onSuccess={() => {
loadData();
// 先关闭弹窗
setEditBasicInfoVisible(false);
// 延迟清空 record 和刷新数据,等待 Dialog 关闭动画完成
setTimeout(() => {
setEditBasicInfoRecord(null);
loadData();
}, 300);
}}
/>

View File

@ -89,13 +89,18 @@ const router = createBrowserRouter([
path: 'applications',
element: <Suspense fallback={<LoadingComponent/>}><ApplicationList/></Suspense>
},
{
path: 'environments',
element: <Suspense fallback={<LoadingComponent/>}><EnvironmentList/></Suspense>
},
{
path: 'deployment',
element: <Suspense fallback={<LoadingComponent/>}><DeploymentConfigList/></Suspense>
}
]
},
{
path: 'resource',
children: [
{
path: 'environments',
element: <Suspense fallback={<LoadingComponent/>}><EnvironmentList/></Suspense>
},
{
path: 'jenkins-manager',
@ -152,51 +157,6 @@ const router = createBrowserRouter([
}
]
},
{
path: 'form',
children: [
{
path: 'definitions',
element: (
<Suspense fallback={<LoadingComponent/>}>
<FormDefinitionList/>
</Suspense>
)
},
{
path: 'definitions/create',
element: (
<Suspense fallback={<LoadingComponent/>}>
<FormDefinitionDesigner/>
</Suspense>
)
},
{
path: 'definitions/:id/design',
element: (
<Suspense fallback={<LoadingComponent/>}>
<FormDefinitionDesigner/>
</Suspense>
)
},
{
path: 'data',
element: (
<Suspense fallback={<LoadingComponent/>}>
<FormDataList/>
</Suspense>
)
},
{
path: 'data/:id',
element: (
<Suspense fallback={<LoadingComponent/>}>
<FormDataDetail/>
</Suspense>
)
}
]
},
{
path: 'workflow',
children: [
@ -229,6 +189,51 @@ const router = createBrowserRouter([
</Suspense>
)
},
{
path: 'form',
children: [
{
index: true,
element: (
<Suspense fallback={<LoadingComponent/>}>
<FormDefinitionList/>
</Suspense>
)
},
{
path: 'create',
element: (
<Suspense fallback={<LoadingComponent/>}>
<FormDefinitionDesigner/>
</Suspense>
)
},
{
path: ':id/design',
element: (
<Suspense fallback={<LoadingComponent/>}>
<FormDefinitionDesigner/>
</Suspense>
)
},
{
path: 'data',
element: (
<Suspense fallback={<LoadingComponent/>}>
<FormDataList/>
</Suspense>
)
},
{
path: 'data/:id',
element: (
<Suspense fallback={<LoadingComponent/>}>
<FormDataDetail/>
</Suspense>
)
}
]
},
{
path: 'node-design',
children: [

View File

@ -1,5 +1,5 @@
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
import {message} from 'antd';
import {toast} from '@/components/ui/use-toast';
export interface Response<T = any> {
code: number;
@ -46,7 +46,11 @@ const responseHandler = (response: AxiosResponse<Response<any>>) => {
return result.data;
} else {
if (result.message != undefined) {
message.error(result.message || defaultErrorMessage);
toast({
title: '操作失败',
description: result.message || defaultErrorMessage,
variant: 'destructive'
});
return Promise.reject(response);
}
return Promise.reject(response);
@ -56,7 +60,11 @@ const responseHandler = (response: AxiosResponse<Response<any>>) => {
const errorHandler = (error: any) => {
if (!error.response) {
message.error('网络连接异常,请检查网络');
toast({
title: '网络错误',
description: '网络连接异常,请检查网络',
variant: 'destructive'
});
return Promise.reject(error);
}
@ -70,7 +78,11 @@ const errorHandler = (error: any) => {
case 401:
// 登录已过期,清除所有本地存储并跳转到登录页
errorMessage = '登录已过期,请重新登录';
message.error(errorMessage);
toast({
title: '登录已过期',
description: errorMessage,
variant: 'destructive'
});
// 清除本地存储的所有用户相关信息
localStorage.removeItem('token');
@ -85,19 +97,35 @@ const errorHandler = (error: any) => {
break;
case 403:
errorMessage = '拒绝访问';
message.error(errorMessage);
toast({
title: '访问被拒绝',
description: errorMessage,
variant: 'destructive'
});
break;
case 404:
errorMessage = '请求错误,未找到该资源';
message.error(errorMessage);
toast({
title: '请求错误',
description: errorMessage,
variant: 'destructive'
});
break;
case 500:
errorMessage = '服务异常,请稍后再试';
message.error(errorMessage);
toast({
title: '服务器错误',
description: errorMessage,
variant: 'destructive'
});
break;
default:
errorMessage = '服务器异常,请稍后再试!';
message.error(errorMessage);
toast({
title: '请求失败',
description: errorMessage,
variant: 'destructive'
});
}
return Promise.reject(error);