diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3e2543ef..4f3c537e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,11 +3,13 @@ import { RouterProvider } from 'react-router-dom'; import { ConfigProvider } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import router from './router'; +import { Toaster } from "@/components/ui/toaster"; const App: React.FC = () => { return ( + ); }; diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..27a5c63b --- /dev/null +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(({ className, ...props }, ref) => ( + + + + + + + ); }; diff --git a/frontend/src/pages/Deploy/ProjectGroup/List/index.tsx b/frontend/src/pages/Deploy/ProjectGroup/List/index.tsx index 0cca560d..e3d832fc 100644 --- a/frontend/src/pages/Deploy/ProjectGroup/List/index.tsx +++ b/frontend/src/pages/Deploy/ProjectGroup/List/index.tsx @@ -1,6 +1,5 @@ import React, {useState} from 'react'; import {PageContainer} from '@ant-design/pro-layout'; -import {Button, Space, Popconfirm, Tag, App, Form, Input, Select, Card} from 'antd'; import { PlusOutlined, EditOutlined, @@ -22,6 +21,44 @@ import { TableRow, TableCell, } from "@/components/ui/table"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import {Button} from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form"; +import {Input} from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import {Badge} from "@/components/ui/badge"; +import {useToast} from "@/components/ui/use-toast"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import {useForm} from "react-hook-form"; +import {zodResolver} from "@hookform/resolvers/zod"; +import {searchFormSchema, type SearchFormValues} from "./schema"; interface Column { accessorKey?: keyof ProjectGroup; @@ -36,8 +73,17 @@ const ProjectGroupList: React.FC = () => { const [currentProject, setCurrentProject] = useState(); const [list, setList] = useState([]); const [loading, setLoading] = useState(false); - const [searchForm] = Form.useForm(); - const {message: messageApi} = App.useApp(); + const {toast} = useToast(); + + const form = useForm({ + resolver: zodResolver(searchFormSchema), + defaultValues: { + projectGroupCode: "", + projectGroupName: "", + type: undefined, + enabled: undefined, + }, + }); const loadData = async (params?: ProjectGroupQueryParams) => { setLoading(true); @@ -45,7 +91,11 @@ const ProjectGroupList: React.FC = () => { const data = await getProjectGroupPage(params); setList(data.content); } catch (error) { - messageApi.error('加载数据失败'); + toast({ + variant: "destructive", + title: "错误", + description: "加载数据失败", + }); } finally { setLoading(false); } @@ -58,10 +108,17 @@ const ProjectGroupList: React.FC = () => { const handleDelete = async (id: number) => { try { await deleteProjectGroup(id); - messageApi.success('删除成功'); - loadData(searchForm.getFieldsValue()); + toast({ + title: "成功", + description: "删除成功", + }); + loadData(form.getValues()); } catch (error) { - messageApi.error('删除失败'); + toast({ + variant: "destructive", + title: "错误", + description: "删除失败", + }); } }; @@ -83,10 +140,10 @@ const ProjectGroupList: React.FC = () => { const handleSuccess = () => { setModalVisible(false); setCurrentProject(undefined); - loadData(searchForm.getFieldsValue()); + loadData(form.getValues()); }; - const handleSearch = async (values: any) => { + const handleSearch = async (values: SearchFormValues) => { loadData(values); }; @@ -113,12 +170,12 @@ const ProjectGroupList: React.FC = () => { cell: ({ row }) => { const typeInfo = getProjectTypeInfo(row.original.type); return ( - - + + {typeInfo.icon} {typeInfo.label} - - + + ); }, }, @@ -127,9 +184,9 @@ const ProjectGroupList: React.FC = () => { header: '状态', size: 100, cell: ({ row }) => ( - + {row.original.enabled ? '启用' : '禁用'} - + ), }, { @@ -137,10 +194,10 @@ const ProjectGroupList: React.FC = () => { header: '环境数量', size: 100, cell: ({ row }) => ( - + {row.original.totalEnvironments || 0} - + ), }, { @@ -148,10 +205,10 @@ const ProjectGroupList: React.FC = () => { header: '项目数量', size: 100, cell: ({ row }) => ( - + {row.original.totalApplications || 0} - + ), }, { @@ -164,131 +221,164 @@ const ProjectGroupList: React.FC = () => { header: '操作', size: 180, cell: ({ row }) => ( - +
- handleDelete(row.original.id)} - > - - - + + + + + 确定要删除该项目组吗? + + 删除后将无法恢复,请谨慎操作 + + + + 取消 + handleDelete(row.original.id)} + > + 确定 + + + + +
), }, ]; return ( - -
- - - - - - - - - - - - - - - -
+ + + +
-
- -
- -
- - - - {columns.map((column) => ( - - {column.header} - - ))} - - - - {list.map((row) => ( - + + +
+
+ + {columns.map((column) => ( - - {column.cell - ? column.cell({ row: { original: row } }) - : column.accessorKey - ? String(row[column.accessorKey]) - : null} - + + {column.header} + ))} - ))} - -
-
+ + + {list.map((item) => ( + + {columns.map((column) => ( + + {column.cell + ? column.cell({ row: { original: item } }) + : item[column.accessorKey!]} + + ))} + + ))} + + +
+ val || ''), + enabled: z.boolean().default(true), + sort: z.number({ + required_error: "排序不能为空", + invalid_type_error: "排序必须是数字", + }) + .int("排序必须是整数") + .min(0, "排序不能小于0") + .default(0), + tenantCode: z.string().default('default'), +}); + +export type SearchFormValues = z.infer; +export type FormValues = z.infer; \ No newline at end of file