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) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} \ No newline at end of file diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index e87d62bf..e2977385 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -4,17 +4,19 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { default: - "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: - "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", outline: "text-foreground", + success: + "border-transparent bg-green-500 text-white hover:bg-green-500/80", }, }, defaultVariants: { diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 65d4fcd9..e71405e4 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -1,30 +1,30 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" +import { Loader2 } from "lucide-react" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", + default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", }, }, defaultVariants: { @@ -37,18 +37,29 @@ const buttonVariants = cva( export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild?: boolean; + loading?: boolean; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { + ({ className, variant, size, asChild = false, loading = false, children, disabled, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( + > + {loading ? ( + <> + + {children} + > + ) : ( + children + )} + ) } ) diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index 9dbeaa09..c680b9d3 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef< , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} \ No newline at end of file diff --git a/frontend/src/components/ui/toaster.tsx b/frontend/src/components/ui/toaster.tsx new file mode 100644 index 00000000..88e645cd --- /dev/null +++ b/frontend/src/components/ui/toaster.tsx @@ -0,0 +1,33 @@ +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + + + {title && {title}} + {description && ( + {description} + )} + + {action} + + + ) + })} + + + ) +} \ No newline at end of file diff --git a/frontend/src/components/ui/use-toast.ts b/frontend/src/components/ui/use-toast.ts new file mode 100644 index 00000000..abae1b3b --- /dev/null +++ b/frontend/src/components/ui/use-toast.ts @@ -0,0 +1,191 @@ +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_VALUE + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } \ No newline at end of file diff --git a/frontend/src/pages/Deploy/ProjectGroup/List/components/ProjectGroupModal.tsx b/frontend/src/pages/Deploy/ProjectGroup/List/components/ProjectGroupModal.tsx index 78dbf518..aa4c2b3b 100644 --- a/frontend/src/pages/Deploy/ProjectGroup/List/components/ProjectGroupModal.tsx +++ b/frontend/src/pages/Deploy/ProjectGroup/List/components/ProjectGroupModal.tsx @@ -1,8 +1,36 @@ -import React, {useState} from 'react'; -import {Modal, Form, Input, Select, Switch, InputNumber, message} from 'antd'; +import React, {useState, useEffect} from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import {Button} from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormDescription, +} from "@/components/ui/form"; +import {Input} from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import {Switch} from "@/components/ui/switch"; +import {useToast} from "@/components/ui/use-toast"; +import {useForm} from "react-hook-form"; +import {zodResolver} from "@hookform/resolvers/zod"; import type {ProjectGroup} from '../types'; import {ProjectGroupTypeEnum} from '../types'; import {createProjectGroup, updateProjectGroup} from '../service'; +import {formSchema, type FormValues} from '../schema'; interface ProjectGroupModalProps { open: boolean; @@ -17,136 +45,243 @@ const ProjectGroupModal: React.FC = ({ onSuccess, initialValues, }) => { - const [form] = Form.useForm(); const [loading, setLoading] = useState(false); + const {toast} = useToast(); const isEdit = !!initialValues?.id; - const handleSubmit = async () => { + const form = useForm({ + resolver: zodResolver(formSchema), + mode: 'onChange', + defaultValues: { + projectGroupCode: '', + projectGroupName: '', + projectGroupDesc: '', + type: undefined, + enabled: true, + sort: 0, + tenantCode: 'default', + }, + }); + + useEffect(() => { + if (open) { + if (initialValues) { + form.reset({ + projectGroupCode: initialValues.projectGroupCode, + projectGroupName: initialValues.projectGroupName, + projectGroupDesc: initialValues.projectGroupDesc || '', + type: initialValues.type, + enabled: initialValues.enabled, + sort: initialValues.sort || 0, + tenantCode: initialValues.tenantCode || 'default', + }); + } else { + form.reset({ + projectGroupCode: '', + projectGroupName: '', + projectGroupDesc: '', + type: undefined, + enabled: true, + sort: 0, + tenantCode: 'default', + }); + } + } + }, [form, initialValues, open]); + + const handleSubmit = async (values: FormValues) => { + if (!form.formState.isValid) { + toast({ + variant: "destructive", + title: "错误", + description: "请检查表单填写是否正确", + }); + return; + } + try { setLoading(true); - const values = await form.validateFields(); if (isEdit) { await updateProjectGroup({ ...values, - id: initialValues.id, + id: initialValues!.id, + }); + toast({ + title: "成功", + description: "项目组更新成功", }); } else { await createProjectGroup(values); + toast({ + title: "成功", + description: "项目组创建成功", + }); } - message.success(`${isEdit ? '更新' : '创建'}成功`); - form.resetFields(); onSuccess(); } catch (error) { - if (error instanceof Error) { - message.error(`${isEdit ? '更新' : '创建'}失败: ${error.message}`); - } else { - message.error(`${isEdit ? '更新' : '创建'}失败`); - } + console.error('操作失败:', error); + toast({ + variant: "destructive", + title: "错误", + description: error instanceof Error ? error.message : `项目组${isEdit ? '更新' : '创建'}失败`, + }); } finally { setLoading(false); } }; return ( - { - form.resetFields(); - onCancel(); - }} - onOk={handleSubmit} - confirmLoading={loading} - maskClosable={false} - destroyOnClose - > - - - - + !open && onCancel()}> + + + {isEdit ? '编辑' : '新建'}项目组 + + + + ( + + 项目组编码 + + + + + 项目组的唯一标识,创建后不可修改。只能包含小写字母、数字和连字符 + + + )} + /> - - - + ( + + 项目组名称 + + + + + 项目组的显示名称 + + + )} + /> - - - 产品型 - 项目型 - - + ( + + 项目组类型 + + + + + + + + 产品型 + 项目型 + + + + 项目组的类型,创建后不可修改 + + + )} + /> - - - + ( + + 项目组描述 + + + + + 项目组的详细描述信息 + + + )} + /> - - - + ( + + + 状态 + + 是否启用该项目组 + + + + + + + )} + /> - - - - - + ( + + 排序 + + field.onChange(Number(e.target.value))} + /> + + + 数值越小越靠前 + + + )} + /> + + + + 取消 + + + 确定 + + + + + + ); }; 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 }) => ( - + handleEdit(row.original)} > - - - 编辑 - + + 编辑 - handleDelete(row.original.id)} - > - - - + + + + 删除 - - - - + + + + + 确定要删除该项目组吗? + + 删除后将无法恢复,请谨慎操作 + + + + 取消 + handleDelete(row.original.id)} + > + 确定 + + + + + ), }, ]; return ( - - - - - - - - - - - - - - - - - }> - 搜索 - - { - searchForm.resetFields(); - handleSearch({}); - }}> + + + 搜索条件 + + + + + ( + + 项目组编码 + + + + + )} + /> + + ( + + 项目组名称 + + + + + )} + /> + + ( + + 项目组类型 + + + + + + + + 产品型 + 项目型 + + + + )} + /> + + form.reset()} + > 重置 - - - + + + 搜索 + + + + - - } - onClick={handleAdd} - > - 新增项目组 + + 项目组列表 + + + 新建 - - - - - - - {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