This commit is contained in:
asp_ly 2024-12-27 23:47:10 +08:00
parent 5eb44c62cb
commit c2c0e3c815
4 changed files with 584 additions and 316 deletions

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -1,8 +1,37 @@
import React, {useState} from 'react';
import {Modal, Form, Input, Select, Switch, InputNumber, message} from 'antd';
import React, {useEffect} from 'react';
import type {Application} from '../types';
import {DevelopmentLanguageTypeEnum} from '../types';
import {createApplication, updateApplication} from '../service';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {Button} from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} 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 {applicationFormSchema, type ApplicationFormValues} from "../schema";
import {Textarea} from "@/components/ui/textarea";
interface ApplicationModalProps {
open: boolean;
@ -19,18 +48,43 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
initialValues,
projectGroupId,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const {toast} = useToast();
const isEdit = !!initialValues?.id;
const handleSubmit = async () => {
const form = useForm<ApplicationFormValues>({
resolver: zodResolver(applicationFormSchema),
defaultValues: {
appCode: "",
appName: "",
appDesc: "",
repoUrl: "",
language: undefined,
enabled: true,
sort: 0,
},
});
useEffect(() => {
if (initialValues) {
form.reset({
appCode: initialValues.appCode,
appName: initialValues.appName,
appDesc: initialValues.appDesc || "",
repoUrl: initialValues.repoUrl,
language: initialValues.language,
enabled: initialValues.enabled,
sort: initialValues.sort,
});
}
}, [initialValues, form]);
const handleSubmit = async (values: ApplicationFormValues) => {
try {
setLoading(true);
const values = await form.validateFields();
if (isEdit) {
await updateApplication({
...values,
id: initialValues.id,
projectGroupId,
});
} else {
await createApplication({
@ -38,136 +92,177 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
projectGroupId,
});
}
message.success(`${isEdit ? '更新' : '创建'}成功`);
form.resetFields();
toast({
title: `${isEdit ? '更新' : '创建'}成功`,
duration: 3000,
});
form.reset();
onSuccess();
} catch (error) {
if (error instanceof Error) {
message.error(`${isEdit ? '更新' : '创建'}失败: ${error.message}`);
} else {
message.error(`${isEdit ? '更新' : '创建'}失败`);
}
} finally {
setLoading(false);
toast({
variant: "destructive",
title: `${isEdit ? '更新' : '创建'}失败`,
description: error instanceof Error ? error.message : undefined,
duration: 3000,
});
}
};
return (
<Modal
title={`${isEdit ? '编辑' : '新建'}应用`}
open={open}
onCancel={() => {
form.resetFields();
onCancel();
}}
onOk={handleSubmit}
confirmLoading={loading}
maskClosable={false}
destroyOnClose
>
<Form
form={form}
layout="vertical"
initialValues={{
enabled: true,
sort: 0,
...initialValues,
}}
>
<Form.Item
<Dialog open={open} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>{isEdit ? '编辑' : '新建'}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="appCode"
label="应用编码"
rules={[
{required: true, message: '请输入应用编码'},
{max: 50, message: '应用编码不能超过50个字符'},
]}
tooltip="应用的唯一标识,创建后不可修改"
>
render={({field}) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
{...field}
disabled={isEdit}
placeholder="请输入应用编码"
disabled={isEdit}
maxLength={50}
/>
</Form.Item>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<Form.Item
<FormField
control={form.control}
name="appName"
label="应用名称"
rules={[
{required: true, message: '请输入应用名称'},
{max: 50, message: '应用名称不能超过50个字符'},
]}
tooltip="应用的显示名称"
>
render={({field}) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
{...field}
placeholder="请输入应用名称"
maxLength={50}
/>
</Form.Item>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<Form.Item
<FormField
control={form.control}
name="language"
label="开发语言"
rules={[{required: true, message: '请选择开发语言'}]}
tooltip="应用的主要开发语言,创建后不可修改"
>
render={({field}) => (
<FormItem>
<FormLabel></FormLabel>
<Select
placeholder="请选择开发语言"
disabled={isEdit}
onValueChange={field.onChange}
value={field.value}
>
<Select.Option value={DevelopmentLanguageTypeEnum.JAVA}>Java</Select.Option>
<Select.Option value={DevelopmentLanguageTypeEnum.NODE_JS}>NodeJS</Select.Option>
<Select.Option value={DevelopmentLanguageTypeEnum.PYTHON}>Python</Select.Option>
<Select.Option value={DevelopmentLanguageTypeEnum.GO}>Go</Select.Option>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="请选择开发语言"/>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={DevelopmentLanguageTypeEnum.JAVA}>Java</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.NODE_JS}>NodeJS</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.PYTHON}>Python</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.GO}>Go</SelectItem>
</SelectContent>
</Select>
</Form.Item>
<FormMessage/>
</FormItem>
)}
/>
<Form.Item
<FormField
control={form.control}
name="repoUrl"
label="仓库地址"
rules={[
{required: true, message: '请输入仓库地址'},
{type: 'url', message: '请输入有效的URL地址'},
]}
tooltip="应用代码仓库的URL地址"
>
render={({field}) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
{...field}
placeholder="请输入仓库地址"
/>
</Form.Item>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<Form.Item
<FormField
control={form.control}
name="appDesc"
label="应用描述"
rules={[{max: 200, message: '应用描述不能超过200个字符'}]}
tooltip="应用的详细描述信息"
>
<Input.TextArea
render={({field}) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Textarea
{...field}
placeholder="请输入应用描述"
maxLength={200}
showCount
rows={4}
/>
</Form.Item>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<Form.Item
<FormField
control={form.control}
name="enabled"
label="状态"
valuePropName="checked"
tooltip="是否启用该应用"
>
<Switch checkedChildren="启用" unCheckedChildren="禁用"/>
</Form.Item>
render={({field}) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel></FormLabel>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<Form.Item
<FormField
control={form.control}
name="sort"
label="排序"
tooltip="数字越小越靠前"
>
<InputNumber min={0} style={{width: '100%'}}/>
</Form.Item>
render={({field}) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
{...field}
type="number"
min={0}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="outline" onClick={onCancel}>
</Button>
<Button type="submit">
</Button>
</DialogFooter>
</form>
</Form>
</Modal>
</DialogContent>
</Dialog>
);
};

View File

@ -1,6 +1,5 @@
import React, {useState, useEffect} from 'react';
import {PageContainer} from '@ant-design/pro-layout';
import {Button, Space, Popconfirm, Tag, Select, App} from 'antd';
import {PageContainer} from '@/components/ui/page-container';
import {
PlusOutlined,
EditOutlined,
@ -19,18 +18,79 @@ import type {ProjectGroup} from '../../ProjectGroup/List/types';
import {ProjectGroupTypeEnum} from '../../ProjectGroup/List/types';
import {getProjectTypeInfo} from '../../ProjectGroup/List/utils';
import ApplicationModal from './components/ApplicationModal';
import {ProTable} from '@ant-design/pro-components';
import type {ProColumns, ActionType} from '@ant-design/pro-components';
import {
Table,
TableHeader,
TableBody,
TableHead,
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";
const {Option} = Select;
interface Column {
accessorKey?: keyof Application;
id?: string;
header: string;
size: number;
cell?: (props: { row: { original: Application } }) => React.ReactNode;
}
const ApplicationList: React.FC = () => {
const [projectGroups, setProjects] = useState<ProjectGroup[]>([]);
const [selectedProjectGroupId, setSelectedProjectGroupId] = useState<number>();
const [modalVisible, setModalVisible] = useState(false);
const [currentApplication, setCurrentApplication] = useState<Application>();
const actionRef = React.useRef<ActionType>();
const {message: messageApi} = App.useApp();
const [list, setList] = useState<Application[]>([]);
const [loading, setLoading] = useState(false);
const {toast} = useToast();
const form = useForm<SearchFormValues>({
resolver: zodResolver(searchFormSchema),
defaultValues: {
appCode: "",
appName: "",
language: undefined,
enabled: undefined,
},
});
// 获取项目列表
const fetchProjects = async () => {
@ -41,7 +101,11 @@ const ApplicationList: React.FC = () => {
setSelectedProjectGroupId(data[0].id);
}
} catch (error) {
messageApi.error('获取项目组列表失败');
toast({
variant: "destructive",
title: "获取项目组列表失败",
duration: 3000,
});
}
};
@ -49,19 +113,58 @@ const ApplicationList: React.FC = () => {
fetchProjects();
}, []);
const loadData = async (params?: ApplicationQuery) => {
if (!selectedProjectGroupId) {
setList([]);
return;
}
setLoading(true);
try {
const queryParams: ApplicationQuery = {
...params,
projectGroupId: selectedProjectGroupId,
};
const data = await getApplicationPage(queryParams);
setList(data.content || []);
} catch (error) {
toast({
variant: "destructive",
title: "获取应用列表失败",
duration: 3000,
});
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData(form.getValues());
}, [selectedProjectGroupId]);
const handleDelete = async (id: number) => {
try {
await deleteApplication(id);
messageApi.success('删除成功');
actionRef.current?.reload();
toast({
title: "删除成功",
duration: 3000,
});
loadData(form.getValues());
} catch (error) {
messageApi.error('删除失败');
toast({
variant: "destructive",
title: "删除失败",
duration: 3000,
});
}
};
const handleAdd = () => {
if (!selectedProjectGroupId) {
messageApi.warning('请先选择项目组');
toast({
variant: "destructive",
title: "请先选择项目组",
duration: 3000,
});
return;
}
setCurrentApplication(undefined);
@ -73,9 +176,8 @@ const ApplicationList: React.FC = () => {
setModalVisible(true);
};
const handleProjectChange = (value: number) => {
setSelectedProjectGroupId(value);
actionRef.current?.reload();
const handleProjectChange = (value: string) => {
setSelectedProjectGroupId(Number(value));
};
const handleModalClose = () => {
@ -86,7 +188,7 @@ const ApplicationList: React.FC = () => {
const handleSuccess = () => {
setModalVisible(false);
setCurrentApplication(undefined);
actionRef.current?.reload();
loadData(form.getValues());
};
// 获取开发语言信息
@ -125,217 +227,242 @@ const ApplicationList: React.FC = () => {
}
};
const columns: ProColumns<Application>[] = [
const columns: Column[] = [
{
title: '应用编码',
dataIndex: 'appCode',
width: 180,
copyable: true,
ellipsis: true,
fixed: 'left',
accessorKey: 'appCode',
header: '应用编码',
size: 180,
},
{
title: '应用名称',
dataIndex: 'appName',
width: 150,
ellipsis: true,
accessorKey: 'appName',
header: '应用名称',
size: 150,
},
{
title: '项目组',
dataIndex: ['projectGroup', 'projectGroupName'],
width: 150,
ellipsis: true,
render: (_, record) => (
<Space>
{record.projectGroup?.projectGroupName}
{record.projectGroup?.type && (
<Tag color={getProjectTypeInfo(record.projectGroup.type).color}>
{getProjectTypeInfo(record.projectGroup.type).label}
</Tag>
id: 'projectGroup',
header: '项目组',
size: 150,
cell: ({row}) => (
<div className="flex items-center gap-2">
<span>{row.original.projectGroup?.projectGroupName}</span>
{row.original.projectGroup?.type && (
<Badge variant="outline">
{getProjectTypeInfo(row.original.projectGroup.type).label}
</Badge>
)}
</Space>
</div>
),
},
{
title: '应用描述',
dataIndex: 'appDesc',
ellipsis: true,
width: 200,
accessorKey: 'appDesc',
header: '应用描述',
size: 200,
},
{
title: '仓库地址',
dataIndex: 'repoUrl',
width: 200,
ellipsis: true,
render: (_, record) => record.repoUrl ? (
<Space>
accessorKey: 'repoUrl',
header: '仓库地址',
size: 200,
cell: ({row}) => row.original.repoUrl ? (
<div className="flex items-center gap-2">
<GithubOutlined/>
<a href={record.repoUrl} target="_blank" rel="noopener noreferrer">
{record.repoUrl}
<a href={row.original.repoUrl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:text-blue-700">
{row.original.repoUrl}
</a>
</Space>
</div>
) : '-',
},
{
title: '开发语言',
dataIndex: 'language',
width: 120,
render: (language) => {
const langInfo = getLanguageInfo(language as DevelopmentLanguageTypeEnum);
accessorKey: 'language',
header: '开发语言',
size: 120,
cell: ({row}) => {
const langInfo = getLanguageInfo(row.original.language);
return (
<Tag color={langInfo.color}>
<Space>
<Badge variant="outline" className="flex items-center gap-1">
{langInfo.icon}
{langInfo.label}
</Space>
</Tag>
</Badge>
);
},
filters: [
{text: 'Java', value: DevelopmentLanguageTypeEnum.JAVA},
{text: 'NodeJS', value: DevelopmentLanguageTypeEnum.NODE_JS},
{text: 'Python', value: DevelopmentLanguageTypeEnum.PYTHON},
{text: 'Go', value: DevelopmentLanguageTypeEnum.GO},
],
filterMode: 'menu',
filtered: false,
},
{
title: '状态',
dataIndex: 'enabled',
width: 100,
valueEnum: {
true: {text: '启用', status: 'Success'},
false: {text: '禁用', status: 'Default'},
},
accessorKey: 'enabled',
header: '状态',
size: 100,
cell: ({row}) => (
<Badge variant={row.original.enabled ? "outline" : "secondary"}>
{row.original.enabled ? '启用' : '禁用'}
</Badge>
),
},
{
title: '排序',
dataIndex: 'sort',
width: 80,
sorter: true,
accessorKey: 'sort',
header: '排序',
size: 80,
},
{
title: '操作',
width: 180,
key: 'action',
valueType: 'option',
fixed: 'right',
render: (_, record) => [
id: 'actions',
header: '操作',
size: 180,
cell: ({row}) => (
<div className="flex items-center gap-2">
<Button
key="edit"
type="link"
onClick={() => handleEdit(record)}
variant="ghost"
size="sm"
onClick={() => handleEdit(row.original)}
>
<Space>
<EditOutlined/>
<EditOutlined className="mr-1"/>
</Space>
</Button>,
<Popconfirm
key="delete"
title="确定要删除该应用吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(record.id)}
>
<Button
type="link"
danger
>
<Space>
<DeleteOutlined/>
</Space>
</Button>
</Popconfirm>
],
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-destructive"
>
<DeleteOutlined className="mr-1"/>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDelete(row.original.id)}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
),
},
];
return (
<PageContainer
header={{
title: '应用管理',
extra: [
<PageContainer>
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight"></h2>
<div className="flex items-center gap-4">
<Select
key="project-select"
value={selectedProjectGroupId}
onChange={handleProjectChange}
style={{width: 200}}
placeholder="请选择项目组"
value={selectedProjectGroupId?.toString()}
onValueChange={handleProjectChange}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="请选择项目组"/>
</SelectTrigger>
<SelectContent>
{projectGroups.map((project) => (
<Option key={project.id} value={project.id}>
<SelectItem key={project.id} value={project.id.toString()}>
{project.projectGroupName}
</Option>
</SelectItem>
))}
</Select>,
],
}}
>
<ProTable<Application>
columns={columns}
actionRef={actionRef}
scroll={{x: 'max-content'}}
cardBordered
rowKey="id"
search={false}
options={{
setting: false,
density: false,
fullScreen: false,
reload: false,
}}
toolbar={{
actions: [
<Button
key="add"
type="primary"
onClick={handleAdd}
icon={<PlusOutlined/>}
disabled={!selectedProjectGroupId}
>
</SelectContent>
</Select>
<Button onClick={handleAdd} disabled={!selectedProjectGroupId}>
<PlusOutlined className="mr-1"/>
</Button>
],
}}
pagination={{
pageSize: 10,
showQuickJumper: true,
}}
request={async (params) => {
if (!selectedProjectGroupId) {
return {
data: [],
success: true,
total: 0,
};
}
try {
const queryParams: ApplicationQuery = {
pageSize: params.pageSize,
pageNum: params.current,
projectGroupId: selectedProjectGroupId,
appCode: params.appCode as string,
appName: params.appName as string,
enabled: params.enabled as boolean,
};
const data = await getApplicationPage(queryParams);
return {
data: data.content || [],
success: true,
total: data.totalElements || 0,
};
} catch (error) {
messageApi.error('获取应用列表失败');
return {
data: [],
success: false,
total: 0,
};
}
}}
</div>
</div>
<Card>
<div className="p-6">
<div className="flex items-center gap-4">
<Input
placeholder="应用编码"
value={form.getValues('appCode')}
onChange={(e) => form.setValue('appCode', e.target.value)}
className="max-w-[200px]"
/>
<Input
placeholder="应用名称"
value={form.getValues('appName')}
onChange={(e) => form.setValue('appName', e.target.value)}
className="max-w-[200px]"
/>
<Select
value={form.getValues('language')}
onValueChange={(value) => form.setValue('language', value as DevelopmentLanguageTypeEnum)}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="开发语言"/>
</SelectTrigger>
<SelectContent>
<SelectItem value={DevelopmentLanguageTypeEnum.JAVA}>Java</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.NODE_JS}>NodeJS</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.PYTHON}>Python</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.GO}>Go</SelectItem>
</SelectContent>
</Select>
<Select
value={form.getValues('enabled')?.toString()}
onValueChange={(value) => form.setValue('enabled', value === 'true')}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="状态"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="true"></SelectItem>
<SelectItem value="false"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={() => form.reset()}>
</Button>
<Button variant="ghost" onClick={() => loadData(form.getValues())}>
</Button>
</div>
</div>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead
key={column.accessorKey || column.id}
style={{width: column.size}}
>
{column.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{list.map((item) => (
<TableRow key={item.id}>
{columns.map((column) => (
<TableCell
key={column.accessorKey || column.id}
>
{column.cell
? column.cell({row: {original: item}})
: item[column.accessorKey!]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{modalVisible && selectedProjectGroupId && (
<ApplicationModal

View File

@ -0,0 +1,24 @@
import * as z from "zod";
import { DevelopmentLanguageTypeEnum } from "./types";
export const searchFormSchema = z.object({
appCode: z.string().optional(),
appName: z.string().optional(),
language: z.nativeEnum(DevelopmentLanguageTypeEnum).optional(),
enabled: z.boolean().optional(),
});
export const applicationFormSchema = z.object({
appCode: z.string().min(1, "请输入应用编码").max(50, "应用编码不能超过50个字符"),
appName: z.string().min(1, "请输入应用名称").max(50, "应用名称不能超过50个字符"),
appDesc: z.string().max(200, "应用描述不能超过200个字符").optional(),
repoUrl: z.string().url("请输入有效的URL地址").min(1, "请输入仓库地址"),
language: z.nativeEnum(DevelopmentLanguageTypeEnum, {
required_error: "请选择开发语言",
}),
enabled: z.boolean().default(true),
sort: z.number().min(0).default(0),
});
export type SearchFormValues = z.infer<typeof searchFormSchema>;
export type ApplicationFormValues = z.infer<typeof applicationFormSchema>;