表单CRUD
This commit is contained in:
parent
951fcbfebb
commit
d22285bc95
199
frontend/src/components/ui/dropdown-menu.tsx
Normal file
199
frontend/src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
}
|
||||||
46
frontend/src/pages/Form/Category/service.ts
Normal file
46
frontend/src/pages/Form/Category/service.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import type {
|
||||||
|
FormCategoryQuery,
|
||||||
|
FormCategoryResponse,
|
||||||
|
FormCategoryRequest,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/forms/categories';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询分类
|
||||||
|
*/
|
||||||
|
export const getCategories = (params?: FormCategoryQuery) =>
|
||||||
|
request.get<Page<FormCategoryResponse>>(`${BASE_URL}/page`, { params });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分类详情
|
||||||
|
*/
|
||||||
|
export const getCategoryById = (id: number) =>
|
||||||
|
request.get<FormCategoryResponse>(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有启用的分类(常用)
|
||||||
|
*/
|
||||||
|
export const getEnabledCategories = () =>
|
||||||
|
request.get<FormCategoryResponse[]>(`${BASE_URL}/enabled`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分类
|
||||||
|
*/
|
||||||
|
export const createCategory = (data: FormCategoryRequest) =>
|
||||||
|
request.post<FormCategoryResponse>(BASE_URL, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分类
|
||||||
|
*/
|
||||||
|
export const updateCategory = (id: number, data: FormCategoryRequest) =>
|
||||||
|
request.put<FormCategoryResponse>(`${BASE_URL}/${id}`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分类
|
||||||
|
*/
|
||||||
|
export const deleteCategory = (id: number) =>
|
||||||
|
request.delete(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
40
frontend/src/pages/Form/Category/types.ts
Normal file
40
frontend/src/pages/Form/Category/types.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { BaseQuery } from '@/types/base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类查询参数
|
||||||
|
*/
|
||||||
|
export interface FormCategoryQuery extends BaseQuery {
|
||||||
|
name?: string;
|
||||||
|
code?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类响应
|
||||||
|
*/
|
||||||
|
export interface FormCategoryResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
sort: number;
|
||||||
|
enabled: boolean;
|
||||||
|
createBy?: string;
|
||||||
|
createTime?: string;
|
||||||
|
updateBy?: string;
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单分类创建/更新请求
|
||||||
|
*/
|
||||||
|
export interface FormCategoryRequest {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
sort?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
150
frontend/src/pages/Form/Data/Detail.tsx
Normal file
150
frontend/src/pages/Form/Data/Detail.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { FormRenderer } from '@/components/FormDesigner';
|
||||||
|
import { ArrowLeft, Loader2 } from 'lucide-react';
|
||||||
|
import { getFormDataById } from './service';
|
||||||
|
import type { FormDataResponse, FormDataStatus, FormDataBusinessType } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据详情页
|
||||||
|
*/
|
||||||
|
const FormDataDetail: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<FormDataResponse | null>(null);
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadData(Number(id));
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const loadData = async (dataId: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await getFormDataById(dataId);
|
||||||
|
setData(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载表单数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回列表
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate('/form/data');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态徽章
|
||||||
|
const getStatusBadge = (status: FormDataStatus) => {
|
||||||
|
const statusMap: Record<FormDataStatus, { variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline'; text: string }> = {
|
||||||
|
DRAFT: { variant: 'outline', text: '草稿' },
|
||||||
|
SUBMITTED: { variant: 'success', text: '已提交' },
|
||||||
|
COMPLETED: { variant: 'default', text: '已完成' },
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[status];
|
||||||
|
return <Badge variant={statusInfo.variant}>{statusInfo.text}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 业务类型徽章
|
||||||
|
const getBusinessTypeBadge = (type: FormDataBusinessType) => {
|
||||||
|
const typeMap: Record<FormDataBusinessType, { variant: 'default' | 'secondary' | 'outline'; text: string }> = {
|
||||||
|
STANDALONE: { variant: 'outline', text: '独立' },
|
||||||
|
WORKFLOW: { variant: 'default', text: '工作流' },
|
||||||
|
ORDER: { variant: 'secondary', text: '订单' },
|
||||||
|
};
|
||||||
|
const typeInfo = typeMap[type];
|
||||||
|
return <Badge variant={typeInfo.variant}>{typeInfo.text}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 描述项组件
|
||||||
|
const DescriptionItem: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => (
|
||||||
|
<div className="flex py-3 border-b last:border-b-0">
|
||||||
|
<div className="w-32 text-muted-foreground flex-shrink-0">{label}</div>
|
||||||
|
<div className="flex-1 font-medium">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-12">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin mr-2" />
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-12">
|
||||||
|
<div className="text-center text-muted-foreground">数据不存在</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<Card className="mb-4">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle>表单数据详情</CardTitle>
|
||||||
|
<Button variant="outline" onClick={handleBack}>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
返回列表
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-0">
|
||||||
|
<DescriptionItem label="表单标识" value={data.formKey} />
|
||||||
|
<DescriptionItem label="表单版本" value={`v${data.formVersion}`} />
|
||||||
|
<DescriptionItem label="业务类型" value={getBusinessTypeBadge(data.businessType)} />
|
||||||
|
<DescriptionItem label="业务标识" value={data.businessKey || '-'} />
|
||||||
|
<DescriptionItem label="提交人" value={data.submitter || '-'} />
|
||||||
|
<DescriptionItem label="提交时间" value={data.submitTime || '-'} />
|
||||||
|
<DescriptionItem label="状态" value={getStatusBadge(data.status)} />
|
||||||
|
<DescriptionItem label="创建时间" value={data.createTime || '-'} />
|
||||||
|
<DescriptionItem label="更新时间" value={data.updateTime || '-'} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>表单数据</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{/* 使用 FormRenderer 以只读模式展示数据 */}
|
||||||
|
<FormRenderer
|
||||||
|
schema={data.schemaSnapshot}
|
||||||
|
value={data.data}
|
||||||
|
readonly={true}
|
||||||
|
showSubmit={false}
|
||||||
|
showCancel={true}
|
||||||
|
cancelText="返回"
|
||||||
|
onCancel={handleBack}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormDataDetail;
|
||||||
|
|
||||||
408
frontend/src/pages/Form/Data/index.tsx
Normal file
408
frontend/src/pages/Form/Data/index.tsx
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useNavigate, useSearchParams } 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 { DataTablePagination } from '@/components/ui/pagination';
|
||||||
|
import {
|
||||||
|
Loader2, Search, Eye, Trash2, Download, Folder,
|
||||||
|
Activity, Clock, CheckCircle2, FileCheck, Database
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { getFormDataList, deleteFormData, exportFormData } from './service';
|
||||||
|
import { getEnabledCategories } from '../Category/service';
|
||||||
|
import type { FormDataResponse, FormDataStatus, FormDataBusinessType } from './types';
|
||||||
|
import type { FormCategoryResponse } from '../Category/types';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据列表页
|
||||||
|
*/
|
||||||
|
const FormDataList: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<Page<FormDataResponse> | null>(null);
|
||||||
|
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
|
||||||
|
const [query, setQuery] = useState({
|
||||||
|
pageNum: DEFAULT_CURRENT - 1,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
formDefinitionId: searchParams.get('formDefinitionId') ? Number(searchParams.get('formDefinitionId')) : undefined,
|
||||||
|
businessKey: '',
|
||||||
|
categoryId: undefined as number | undefined,
|
||||||
|
status: undefined as FormDataStatus | undefined,
|
||||||
|
businessType: undefined as FormDataBusinessType | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载分类列表
|
||||||
|
const loadCategories = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getEnabledCategories();
|
||||||
|
setCategories(result || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await getFormDataList(query);
|
||||||
|
setData(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载表单数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCategories();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
setQuery(prev => ({
|
||||||
|
...prev,
|
||||||
|
pageNum: 0,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const handleReset = () => {
|
||||||
|
setQuery({
|
||||||
|
pageNum: 0,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
formDefinitionId: undefined,
|
||||||
|
businessKey: '',
|
||||||
|
categoryId: undefined,
|
||||||
|
status: undefined,
|
||||||
|
businessType: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据分类 ID 获取分类信息
|
||||||
|
const getCategoryInfo = (categoryId?: number) => {
|
||||||
|
return categories.find(cat => cat.id === categoryId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = (record: FormDataResponse) => {
|
||||||
|
navigate(`/form/data/${record.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = async (record: FormDataResponse) => {
|
||||||
|
if (!confirm('确定要删除该数据吗?')) return;
|
||||||
|
try {
|
||||||
|
await deleteFormData(record.id);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
await exportFormData(query);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态徽章
|
||||||
|
const getStatusBadge = (status: FormDataStatus) => {
|
||||||
|
const statusMap: Record<FormDataStatus, {
|
||||||
|
variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline';
|
||||||
|
text: string;
|
||||||
|
icon: React.ElementType
|
||||||
|
}> = {
|
||||||
|
DRAFT: { variant: 'outline', text: '草稿', icon: Clock },
|
||||||
|
SUBMITTED: { variant: 'success', text: '已提交', icon: CheckCircle2 },
|
||||||
|
COMPLETED: { variant: 'default', text: '已完成', icon: FileCheck },
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[status];
|
||||||
|
const Icon = statusInfo.icon;
|
||||||
|
return (
|
||||||
|
<Badge variant={statusInfo.variant} className="flex items-center gap-1">
|
||||||
|
<Icon className="h-3 w-3" />
|
||||||
|
{statusInfo.text}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 业务类型徽章
|
||||||
|
const getBusinessTypeBadge = (type: FormDataBusinessType) => {
|
||||||
|
const typeMap: Record<FormDataBusinessType, { variant: 'default' | 'secondary' | 'outline'; text: string }> = {
|
||||||
|
STANDALONE: { variant: 'outline', text: '独立' },
|
||||||
|
WORKFLOW: { variant: 'default', text: '工作流' },
|
||||||
|
ORDER: { variant: 'secondary', text: '订单' },
|
||||||
|
};
|
||||||
|
const typeInfo = typeMap[type];
|
||||||
|
return <Badge variant={typeInfo.variant}>{typeInfo.text}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const total = data?.totalElements || 0;
|
||||||
|
const draftCount = data?.content?.filter(d => d.status === 'DRAFT').length || 0;
|
||||||
|
const submittedCount = data?.content?.filter(d => d.status === 'SUBMITTED').length || 0;
|
||||||
|
const completedCount = data?.content?.filter(d => d.status === 'COMPLETED').length || 0;
|
||||||
|
return { total, draftCount, submittedCount, completedCount };
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const pageCount = data?.totalElements ? Math.ceil(data.totalElements / query.pageSize) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">表单数据管理</h1>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
查看和管理用户提交的表单数据,支持按分类、状态、业务类型筛选。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 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.submittedCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">待处理的数据</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-gradient-to-br from-purple-500/10 to-purple-500/5 border-purple-500/20">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-purple-700">已完成</CardTitle>
|
||||||
|
<FileCheck className="h-4 w-4 text-purple-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.completedCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">已处理完成</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>数据列表</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||||
|
<div className="flex-1 max-w-md">
|
||||||
|
<Input
|
||||||
|
placeholder="搜索业务标识"
|
||||||
|
value={query.businessKey}
|
||||||
|
onChange={(e) => setQuery(prev => ({ ...prev, businessKey: 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()}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{cat.icon && <Folder className="h-3.5 w-3.5" />}
|
||||||
|
{cat.name}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={query.businessType || undefined}
|
||||||
|
onValueChange={(value) => setQuery(prev => ({ ...prev, businessType: value as FormDataBusinessType }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[140px] h-9">
|
||||||
|
<SelectValue placeholder="全部类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="STANDALONE">独立</SelectItem>
|
||||||
|
<SelectItem value="WORKFLOW">工作流</SelectItem>
|
||||||
|
<SelectItem value="ORDER">订单</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={query.status || undefined}
|
||||||
|
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value as FormDataStatus }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[130px] h-9">
|
||||||
|
<SelectValue placeholder="全部状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="DRAFT">草稿</SelectItem>
|
||||||
|
<SelectItem value="SUBMITTED">已提交</SelectItem>
|
||||||
|
<SelectItem value="COMPLETED">已完成</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>
|
||||||
|
<Button variant="outline" onClick={handleExport} className="h-9">
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表格 */}
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[180px]">表单标识</TableHead>
|
||||||
|
<TableHead className="w-[120px]">分类</TableHead>
|
||||||
|
<TableHead className="w-[100px]">业务类型</TableHead>
|
||||||
|
<TableHead className="w-[150px]">业务标识</TableHead>
|
||||||
|
<TableHead className="w-[100px]">提交人</TableHead>
|
||||||
|
<TableHead className="w-[180px]">提交时间</TableHead>
|
||||||
|
<TableHead className="w-[100px]">状态</TableHead>
|
||||||
|
<TableHead className="text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} 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>
|
||||||
|
) : data?.content && data.content.length > 0 ? (
|
||||||
|
data.content.map((record) => {
|
||||||
|
const categoryInfo = getCategoryInfo(record.categoryId);
|
||||||
|
return (
|
||||||
|
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||||
|
<TableCell>
|
||||||
|
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
|
||||||
|
{record.formKey}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{categoryInfo ? (
|
||||||
|
<Badge variant="outline" className="flex items-center gap-1 w-fit">
|
||||||
|
{categoryInfo.icon && <Folder className="h-3 w-3" />}
|
||||||
|
{categoryInfo.name}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">未分类</Badge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getBusinessTypeBadge(record.businessType)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm">{record.businessKey || '-'}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm">{record.submitter || '匿名'}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm">
|
||||||
|
{record.submitTime ? dayjs(record.submitTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getStatusBadge(record.status)}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleView(record)}
|
||||||
|
title="查看详情"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(record)}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="h-24 text-center">
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
|
<Database 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 + 1}
|
||||||
|
pageSize={query.pageSize}
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={(page) => setQuery(prev => ({
|
||||||
|
...prev,
|
||||||
|
pageNum: page - 1
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormDataList;
|
||||||
|
|
||||||
53
frontend/src/pages/Form/Data/service.ts
Normal file
53
frontend/src/pages/Form/Data/service.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import type {
|
||||||
|
FormDataQuery,
|
||||||
|
FormDataResponse,
|
||||||
|
FormDataSubmitRequest,
|
||||||
|
FormDataUpdateRequest,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/forms/data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询表单数据列表
|
||||||
|
*/
|
||||||
|
export const getFormDataList = (params?: FormDataQuery) =>
|
||||||
|
request.get<Page<FormDataResponse>>(`${BASE_URL}/page`, { params });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表单数据详情
|
||||||
|
*/
|
||||||
|
export const getFormDataById = (id: number) =>
|
||||||
|
request.get<FormDataResponse>(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交表单数据
|
||||||
|
*/
|
||||||
|
export const submitFormData = (data: FormDataSubmitRequest) =>
|
||||||
|
request.post<FormDataResponse>(BASE_URL, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新表单数据
|
||||||
|
*/
|
||||||
|
export const updateFormData = (id: number, data: FormDataUpdateRequest) =>
|
||||||
|
request.put<FormDataResponse>(`${BASE_URL}/${id}`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除表单数据
|
||||||
|
*/
|
||||||
|
export const deleteFormData = (id: number) =>
|
||||||
|
request.delete(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除表单数据
|
||||||
|
*/
|
||||||
|
export const batchDeleteFormData = (ids: number[]) =>
|
||||||
|
request.post(`${BASE_URL}/batch-delete`, { ids });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出表单数据
|
||||||
|
*/
|
||||||
|
export const exportFormData = (params?: FormDataQuery) =>
|
||||||
|
request.download(`${BASE_URL}/export`, undefined, { params });
|
||||||
|
|
||||||
70
frontend/src/pages/Form/Data/types.ts
Normal file
70
frontend/src/pages/Form/Data/types.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { BaseQuery } from '@/types/base';
|
||||||
|
import type { FormSchema } from '@/components/FormDesigner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务类型
|
||||||
|
*/
|
||||||
|
export type FormDataBusinessType = 'STANDALONE' | 'WORKFLOW' | 'ORDER';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据状态
|
||||||
|
*/
|
||||||
|
export type FormDataStatus = 'DRAFT' | 'SUBMITTED' | 'COMPLETED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据查询参数
|
||||||
|
*/
|
||||||
|
export interface FormDataQuery extends BaseQuery {
|
||||||
|
formDefinitionId?: number;
|
||||||
|
formKey?: string;
|
||||||
|
categoryId?: number; // 分类ID(筛选)
|
||||||
|
businessType?: FormDataBusinessType;
|
||||||
|
businessKey?: string;
|
||||||
|
submitter?: string;
|
||||||
|
status?: FormDataStatus;
|
||||||
|
submitTimeStart?: string;
|
||||||
|
submitTimeEnd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据响应
|
||||||
|
*/
|
||||||
|
export interface FormDataResponse {
|
||||||
|
id: number;
|
||||||
|
formDefinitionId: number;
|
||||||
|
formKey: string;
|
||||||
|
formVersion: number;
|
||||||
|
categoryId?: number; // 分类ID
|
||||||
|
businessKey?: string;
|
||||||
|
businessType: FormDataBusinessType;
|
||||||
|
data: Record<string, any>;
|
||||||
|
schemaSnapshot: FormSchema;
|
||||||
|
submitter?: string;
|
||||||
|
submitTime?: string;
|
||||||
|
status: FormDataStatus;
|
||||||
|
createBy?: string;
|
||||||
|
createTime?: string;
|
||||||
|
updateBy?: string;
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据提交请求
|
||||||
|
*/
|
||||||
|
export interface FormDataSubmitRequest {
|
||||||
|
formDefinitionId: number;
|
||||||
|
formKey: string;
|
||||||
|
businessType?: FormDataBusinessType;
|
||||||
|
businessKey?: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
status?: FormDataStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据更新请求
|
||||||
|
*/
|
||||||
|
export interface FormDataUpdateRequest {
|
||||||
|
data: Record<string, any>;
|
||||||
|
status?: FormDataStatus;
|
||||||
|
}
|
||||||
|
|
||||||
263
frontend/src/pages/Form/Definition/Designer.tsx
Normal file
263
frontend/src/pages/Form/Definition/Designer.tsx
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent, CardDescription } 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 type { FormDefinitionRequest } from './types';
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载表单定义
|
||||||
|
useEffect(() => {
|
||||||
|
loadCategories();
|
||||||
|
if (isEdit && id) {
|
||||||
|
loadFormDefinition(Number(id));
|
||||||
|
}
|
||||||
|
}, [id, isEdit]);
|
||||||
|
|
||||||
|
const loadFormDefinition = async (definitionId: number) => {
|
||||||
|
try {
|
||||||
|
const result = await getDefinitionById(definitionId);
|
||||||
|
setFormMeta({
|
||||||
|
name: result.name,
|
||||||
|
key: result.key,
|
||||||
|
categoryId: result.categoryId,
|
||||||
|
description: result.description || '',
|
||||||
|
});
|
||||||
|
setFormSchema(result.schema);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载表单定义失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存表单
|
||||||
|
const handleSave = async (schema: FormSchema) => {
|
||||||
|
if (!formMeta.name.trim()) {
|
||||||
|
alert('请输入表单名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formMeta.key.trim()) {
|
||||||
|
alert('请输入表单标识');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request: FormDefinitionRequest = {
|
||||||
|
name: formMeta.name,
|
||||||
|
key: formMeta.key,
|
||||||
|
categoryId: formMeta.categoryId,
|
||||||
|
description: formMeta.description,
|
||||||
|
schema,
|
||||||
|
status: 'PUBLISHED',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEdit && id) {
|
||||||
|
await updateDefinition(Number(id), request);
|
||||||
|
} else {
|
||||||
|
await createDefinition(request);
|
||||||
|
}
|
||||||
|
navigate('/form/definitions');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存表单失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回列表
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate('/form/definitions');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 p-6">
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
{isEdit ? '编辑表单定义' : '创建表单定义'}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
{isEdit ? '修改表单的基本信息和字段配置' : '设计您的自定义表单,添加字段并配置验证规则'}
|
||||||
|
</p>
|
||||||
|
</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}
|
||||||
|
onChange={setFormSchema}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormDesignerPage;
|
||||||
571
frontend/src/pages/Form/Definition/index.tsx
Normal file
571
frontend/src/pages/Form/Definition/index.tsx
Normal file
@ -0,0 +1,571 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent, CardDescription } 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 { FormRenderer } from '@/components/FormDesigner';
|
||||||
|
import {
|
||||||
|
Loader2, Plus, Search, Eye, Edit, FileText, Ban, Trash2,
|
||||||
|
Database, MoreHorizontal, CheckCircle2, XCircle, Clock, AlertCircle,
|
||||||
|
Folder, Activity
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { getDefinitions, publishDefinition, disableDefinition, deleteDefinition } from './service';
|
||||||
|
import { getEnabledCategories } from '../Category/service';
|
||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义列表页
|
||||||
|
*/
|
||||||
|
const FormDefinitionList: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<Page<FormDefinitionResponse> | null>(null);
|
||||||
|
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
|
||||||
|
const [query, setQuery] = useState({
|
||||||
|
pageNum: DEFAULT_CURRENT - 1,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
name: '',
|
||||||
|
categoryId: undefined as number | undefined,
|
||||||
|
status: undefined as FormDefinitionStatus | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 预览弹窗
|
||||||
|
const [previewVisible, setPreviewVisible] = useState(false);
|
||||||
|
const [previewForm, setPreviewForm] = useState<FormDefinitionResponse | null>(null);
|
||||||
|
|
||||||
|
// 删除确认弹窗
|
||||||
|
const [deleteVisible, setDeleteVisible] = useState(false);
|
||||||
|
const [deleteForm, setDeleteForm] = useState<FormDefinitionResponse | null>(null);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
|
// 加载分类列表
|
||||||
|
const loadCategories = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getEnabledCategories();
|
||||||
|
setCategories(result || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await getDefinitions(query);
|
||||||
|
setData(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载表单定义失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCategories();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const total = data?.totalElements || 0;
|
||||||
|
const draftCount = data?.content?.filter(item => item.status === 'DRAFT').length || 0;
|
||||||
|
const publishedCount = data?.content?.filter(item => item.status === 'PUBLISHED').length || 0;
|
||||||
|
const disabledCount = data?.content?.filter(item => item.status === 'DISABLED').length || 0;
|
||||||
|
|
||||||
|
return { total, draftCount, publishedCount, disabledCount };
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
setQuery(prev => ({
|
||||||
|
...prev,
|
||||||
|
pageNum: 0,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const handleReset = () => {
|
||||||
|
setQuery({
|
||||||
|
pageNum: 0,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
name: '',
|
||||||
|
categoryId: undefined,
|
||||||
|
status: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建表单
|
||||||
|
const handleCreate = () => {
|
||||||
|
navigate('/form/definitions/create');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑表单
|
||||||
|
const handleEdit = (record: FormDefinitionResponse) => {
|
||||||
|
navigate(`/form/definitions/${record.id}/edit`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 预览表单
|
||||||
|
const handlePreview = (record: FormDefinitionResponse) => {
|
||||||
|
setPreviewForm(record);
|
||||||
|
setPreviewVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发布表单
|
||||||
|
const handlePublish = async (record: FormDefinitionResponse) => {
|
||||||
|
try {
|
||||||
|
await publishDefinition(record.id);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发布表单失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 禁用表单
|
||||||
|
const handleDisable = async (record: FormDefinitionResponse) => {
|
||||||
|
try {
|
||||||
|
await disableDefinition(record.id);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('禁用表单失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开删除确认弹窗
|
||||||
|
const handleDeleteClick = (record: FormDefinitionResponse) => {
|
||||||
|
setDeleteForm(record);
|
||||||
|
setDeleteVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!deleteForm) return;
|
||||||
|
setDeleting(true);
|
||||||
|
try {
|
||||||
|
await deleteDefinition(deleteForm.id);
|
||||||
|
setDeleteVisible(false);
|
||||||
|
setDeleteForm(null);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除表单失败:', error);
|
||||||
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看数据
|
||||||
|
const handleViewData = (record: FormDefinitionResponse) => {
|
||||||
|
navigate(`/form/data?formDefinitionId=${record.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据分类 ID 获取分类信息
|
||||||
|
const getCategoryInfo = (categoryId?: number) => {
|
||||||
|
return categories.find(cat => cat.id === categoryId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态徽章(带图标)
|
||||||
|
const getStatusBadge = (status: FormDefinitionStatus) => {
|
||||||
|
const statusMap: Record<FormDefinitionStatus, {
|
||||||
|
variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline';
|
||||||
|
text: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}> = {
|
||||||
|
DRAFT: {
|
||||||
|
variant: 'outline',
|
||||||
|
text: '草稿',
|
||||||
|
icon: <Clock className="h-3 w-3 mr-1" />
|
||||||
|
},
|
||||||
|
PUBLISHED: {
|
||||||
|
variant: 'success',
|
||||||
|
text: '已发布',
|
||||||
|
icon: <CheckCircle2 className="h-3 w-3 mr-1" />
|
||||||
|
},
|
||||||
|
DISABLED: {
|
||||||
|
variant: 'secondary',
|
||||||
|
text: '已禁用',
|
||||||
|
icon: <XCircle className="h-3 w-3 mr-1" />
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[status];
|
||||||
|
return (
|
||||||
|
<Badge variant={statusInfo.variant} className="flex items-center w-fit">
|
||||||
|
{statusInfo.icon}
|
||||||
|
{statusInfo.text}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageCount = data?.totalElements ? Math.ceil(data.totalElements / query.pageSize) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 p-6">
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">总表单数</CardTitle>
|
||||||
|
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.total}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
全部表单定义
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">草稿</CardTitle>
|
||||||
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.draftCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
待发布的表单
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">已发布</CardTitle>
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.publishedCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
正在使用的表单
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">已禁用</CardTitle>
|
||||||
|
<XCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.disabledCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
已停用的表单
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>表单定义管理</CardTitle>
|
||||||
|
<CardDescription className="mt-2">
|
||||||
|
创建和管理表单定义,支持版本控制和发布管理
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleCreate} size="default">
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
创建表单
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<Separator />
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<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()}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{cat.icon && <Folder className="h-3.5 w-3.5" />}
|
||||||
|
{cat.name}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={query.status || undefined}
|
||||||
|
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value as FormDefinitionStatus }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[140px] h-9">
|
||||||
|
<SelectValue placeholder="全部状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="DRAFT">草稿</SelectItem>
|
||||||
|
<SelectItem value="PUBLISHED">已发布</SelectItem>
|
||||||
|
<SelectItem value="DISABLED">已禁用</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button onClick={handleSearch} size="sm">
|
||||||
|
<Search className="h-4 w-4 mr-2" />
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleReset} size="sm">
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表格 */}
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[250px]">表单名称</TableHead>
|
||||||
|
<TableHead className="w-[200px]">表单标识</TableHead>
|
||||||
|
<TableHead className="w-[150px]">分类</TableHead>
|
||||||
|
<TableHead className="w-[80px]">版本</TableHead>
|
||||||
|
<TableHead className="w-[120px]">状态</TableHead>
|
||||||
|
<TableHead className="w-[180px]">更新时间</TableHead>
|
||||||
|
<TableHead className="w-[100px] text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="h-32 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>
|
||||||
|
) : data?.content && data.content.length > 0 ? (
|
||||||
|
data.content.map((record) => {
|
||||||
|
const categoryInfo = getCategoryInfo(record.categoryId);
|
||||||
|
return (
|
||||||
|
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||||
|
<TableCell>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{record.name}</div>
|
||||||
|
{record.description && (
|
||||||
|
<div className="text-xs text-muted-foreground mt-1 line-clamp-1">
|
||||||
|
{record.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<code className="text-xs bg-muted px-2 py-1 rounded">{record.key}</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{categoryInfo ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm">{categoryInfo.name}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">-</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline" className="font-mono">
|
||||||
|
v{record.formVersion}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getStatusBadge(record.status)}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
|
{record.updateTime || record.createTime}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePreview(record)}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-[160px]">
|
||||||
|
<DropdownMenuItem onClick={() => handleViewData(record)}>
|
||||||
|
<Database className="h-4 w-4 mr-2" />
|
||||||
|
查看数据
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{record.status === 'DRAFT' && (
|
||||||
|
<DropdownMenuItem onClick={() => handlePublish(record)}>
|
||||||
|
<FileText className="h-4 w-4 mr-2" />
|
||||||
|
发布表单
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{record.status === 'PUBLISHED' && (
|
||||||
|
<DropdownMenuItem onClick={() => handleDisable(record)}>
|
||||||
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
|
禁用表单
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDeleteClick(record)}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
删除表单
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="h-32 text-center">
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
|
<FileText className="w-12 h-12 mb-4 opacity-20" />
|
||||||
|
<div className="text-sm font-medium">暂无表单定义</div>
|
||||||
|
<div className="text-xs mt-1">点击右上角"创建表单"开始</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{pageCount > 1 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<DataTablePagination
|
||||||
|
pageIndex={query.pageNum + 1}
|
||||||
|
pageSize={query.pageSize}
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={(page) => setQuery(prev => ({
|
||||||
|
...prev,
|
||||||
|
pageNum: page - 1
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 预览弹窗 */}
|
||||||
|
{previewForm && (
|
||||||
|
<Dialog open={previewVisible} onOpenChange={setPreviewVisible}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>预览表单</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{previewForm.name} - {previewForm.key} (v{previewForm.formVersion})
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<div className="py-4">
|
||||||
|
<FormRenderer
|
||||||
|
schema={previewForm.schema}
|
||||||
|
value={{}}
|
||||||
|
readonly={true}
|
||||||
|
showSubmit={false}
|
||||||
|
onCancel={() => setPreviewVisible(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 删除确认弹窗 */}
|
||||||
|
{deleteForm && (
|
||||||
|
<Dialog open={deleteVisible} onOpenChange={setDeleteVisible}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||||
|
确认删除
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
此操作不可撤销,确定要删除表单吗?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="rounded-lg bg-muted p-4 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">表单名称:</span>
|
||||||
|
<span className="text-sm font-medium">{deleteForm.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">表单标识:</span>
|
||||||
|
<code className="text-xs bg-background px-2 py-1 rounded">{deleteForm.key}</code>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">版本:</span>
|
||||||
|
<span className="text-sm">v{deleteForm.formVersion}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setDeleteVisible(false)}
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteConfirm}
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{deleting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormDefinitionList;
|
||||||
58
frontend/src/pages/Form/Definition/service.ts
Normal file
58
frontend/src/pages/Form/Definition/service.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import type {
|
||||||
|
FormDefinitionQuery,
|
||||||
|
FormDefinitionResponse,
|
||||||
|
FormDefinitionRequest,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/forms/definitions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询表单定义列表
|
||||||
|
*/
|
||||||
|
export const getDefinitions = (params?: FormDefinitionQuery) =>
|
||||||
|
request.get<Page<FormDefinitionResponse>>(`${BASE_URL}/page`, { params });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表单定义详情
|
||||||
|
*/
|
||||||
|
export const getDefinitionById = (id: number) =>
|
||||||
|
request.get<FormDefinitionResponse>(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 key 获取表单定义(最新版本)
|
||||||
|
*/
|
||||||
|
export const getDefinitionByKey = (key: string) =>
|
||||||
|
request.get<FormDefinitionResponse>(`${BASE_URL}/by-key/${key}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建表单定义
|
||||||
|
*/
|
||||||
|
export const createDefinition = (data: FormDefinitionRequest) =>
|
||||||
|
request.post<FormDefinitionResponse>(BASE_URL, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新表单定义
|
||||||
|
*/
|
||||||
|
export const updateDefinition = (id: number, data: FormDefinitionRequest) =>
|
||||||
|
request.put<FormDefinitionResponse>(`${BASE_URL}/${id}`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布表单
|
||||||
|
*/
|
||||||
|
export const publishDefinition = (id: number) =>
|
||||||
|
request.post<FormDefinitionResponse>(`${BASE_URL}/${id}/publish`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用表单
|
||||||
|
*/
|
||||||
|
export const disableDefinition = (id: number) =>
|
||||||
|
request.post<FormDefinitionResponse>(`${BASE_URL}/${id}/disable`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除表单定义
|
||||||
|
*/
|
||||||
|
export const deleteDefinition = (id: number) =>
|
||||||
|
request.delete(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
52
frontend/src/pages/Form/Definition/types.ts
Normal file
52
frontend/src/pages/Form/Definition/types.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { BaseQuery } from '@/types/base';
|
||||||
|
import type { FormSchema } from '@/components/FormDesigner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义状态
|
||||||
|
*/
|
||||||
|
export type FormDefinitionStatus = 'DRAFT' | 'PUBLISHED' | 'DISABLED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义查询参数
|
||||||
|
*/
|
||||||
|
export interface FormDefinitionQuery extends BaseQuery {
|
||||||
|
name?: string;
|
||||||
|
key?: string;
|
||||||
|
categoryId?: number; // 分类ID
|
||||||
|
status?: FormDefinitionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义响应
|
||||||
|
*/
|
||||||
|
export interface FormDefinitionResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
formVersion: number;
|
||||||
|
categoryId?: number; // 分类ID
|
||||||
|
description?: string;
|
||||||
|
schema: FormSchema;
|
||||||
|
tags?: string[];
|
||||||
|
status: FormDefinitionStatus;
|
||||||
|
isTemplate: boolean;
|
||||||
|
createBy?: string;
|
||||||
|
createTime?: string;
|
||||||
|
updateBy?: string;
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单定义创建/更新请求
|
||||||
|
*/
|
||||||
|
export interface FormDefinitionRequest {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
categoryId?: number; // 分类ID
|
||||||
|
description?: string;
|
||||||
|
schema: FormSchema;
|
||||||
|
tags?: string[];
|
||||||
|
status?: FormDefinitionStatus;
|
||||||
|
isTemplate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@ -45,6 +45,10 @@ const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/Lis
|
|||||||
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
||||||
const External = lazy(() => import('../pages/Deploy/External'));
|
const External = lazy(() => import('../pages/Deploy/External'));
|
||||||
const FormDesigner = lazy(() => import('../pages/FormDesigner'));
|
const FormDesigner = lazy(() => import('../pages/FormDesigner'));
|
||||||
|
const FormDefinitionList = lazy(() => import('../pages/Form/Definition'));
|
||||||
|
const FormDefinitionDesigner = lazy(() => import('../pages/Form/Definition/Designer'));
|
||||||
|
const FormDataList = lazy(() => import('../pages/Form/Data'));
|
||||||
|
const FormDataDetail = lazy(() => import('../pages/Form/Data/Detail'));
|
||||||
|
|
||||||
// Workflow2 相关路由已迁移到 Workflow,删除旧路由
|
// Workflow2 相关路由已迁移到 Workflow,删除旧路由
|
||||||
|
|
||||||
@ -148,6 +152,51 @@ 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/edit',
|
||||||
|
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',
|
path: 'workflow',
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user