1
This commit is contained in:
parent
5eb44c62cb
commit
c2c0e3c815
22
frontend/src/components/ui/textarea.tsx
Normal file
22
frontend/src/components/ui/textarea.tsx
Normal 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 }
|
||||||
@ -1,8 +1,37 @@
|
|||||||
import React, {useState} from 'react';
|
import React, {useEffect} from 'react';
|
||||||
import {Modal, Form, Input, Select, Switch, InputNumber, message} from 'antd';
|
|
||||||
import type {Application} from '../types';
|
import type {Application} from '../types';
|
||||||
import {DevelopmentLanguageTypeEnum} from '../types';
|
import {DevelopmentLanguageTypeEnum} from '../types';
|
||||||
import {createApplication, updateApplication} from '../service';
|
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 {
|
interface ApplicationModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -19,18 +48,43 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
projectGroupId,
|
projectGroupId,
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const {toast} = useToast();
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const isEdit = !!initialValues?.id;
|
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 {
|
try {
|
||||||
setLoading(true);
|
|
||||||
const values = await form.validateFields();
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
await updateApplication({
|
await updateApplication({
|
||||||
...values,
|
...values,
|
||||||
id: initialValues.id,
|
id: initialValues.id,
|
||||||
|
projectGroupId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createApplication({
|
await createApplication({
|
||||||
@ -38,136 +92,177 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
|||||||
projectGroupId,
|
projectGroupId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
message.success(`${isEdit ? '更新' : '创建'}成功`);
|
toast({
|
||||||
form.resetFields();
|
title: `${isEdit ? '更新' : '创建'}成功`,
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
form.reset();
|
||||||
onSuccess();
|
onSuccess();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
toast({
|
||||||
message.error(`${isEdit ? '更新' : '创建'}失败: ${error.message}`);
|
variant: "destructive",
|
||||||
} else {
|
title: `${isEdit ? '更新' : '创建'}失败`,
|
||||||
message.error(`${isEdit ? '更新' : '创建'}失败`);
|
description: error instanceof Error ? error.message : undefined,
|
||||||
}
|
duration: 3000,
|
||||||
} finally {
|
});
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Dialog open={open} onOpenChange={(open) => !open && onCancel()}>
|
||||||
title={`${isEdit ? '编辑' : '新建'}应用`}
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
open={open}
|
<DialogHeader>
|
||||||
onCancel={() => {
|
<DialogTitle>{isEdit ? '编辑' : '新建'}应用</DialogTitle>
|
||||||
form.resetFields();
|
</DialogHeader>
|
||||||
onCancel();
|
<Form {...form}>
|
||||||
}}
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||||
onOk={handleSubmit}
|
<FormField
|
||||||
confirmLoading={loading}
|
control={form.control}
|
||||||
maskClosable={false}
|
name="appCode"
|
||||||
destroyOnClose
|
render={({field}) => (
|
||||||
>
|
<FormItem>
|
||||||
<Form
|
<FormLabel>应用编码</FormLabel>
|
||||||
form={form}
|
<FormControl>
|
||||||
layout="vertical"
|
<Input
|
||||||
initialValues={{
|
{...field}
|
||||||
enabled: true,
|
disabled={isEdit}
|
||||||
sort: 0,
|
placeholder="请输入应用编码"
|
||||||
...initialValues,
|
/>
|
||||||
}}
|
</FormControl>
|
||||||
>
|
<FormMessage/>
|
||||||
<Form.Item
|
</FormItem>
|
||||||
name="appCode"
|
)}
|
||||||
label="应用编码"
|
/>
|
||||||
rules={[
|
|
||||||
{required: true, message: '请输入应用编码'},
|
|
||||||
{max: 50, message: '应用编码不能超过50个字符'},
|
|
||||||
]}
|
|
||||||
tooltip="应用的唯一标识,创建后不可修改"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入应用编码"
|
|
||||||
disabled={isEdit}
|
|
||||||
maxLength={50}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
<FormField
|
||||||
name="appName"
|
control={form.control}
|
||||||
label="应用名称"
|
name="appName"
|
||||||
rules={[
|
render={({field}) => (
|
||||||
{required: true, message: '请输入应用名称'},
|
<FormItem>
|
||||||
{max: 50, message: '应用名称不能超过50个字符'},
|
<FormLabel>应用名称</FormLabel>
|
||||||
]}
|
<FormControl>
|
||||||
tooltip="应用的显示名称"
|
<Input
|
||||||
>
|
{...field}
|
||||||
<Input
|
placeholder="请输入应用名称"
|
||||||
placeholder="请输入应用名称"
|
/>
|
||||||
maxLength={50}
|
</FormControl>
|
||||||
/>
|
<FormMessage/>
|
||||||
</Form.Item>
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Item
|
<FormField
|
||||||
name="language"
|
control={form.control}
|
||||||
label="开发语言"
|
name="language"
|
||||||
rules={[{required: true, message: '请选择开发语言'}]}
|
render={({field}) => (
|
||||||
tooltip="应用的主要开发语言,创建后不可修改"
|
<FormItem>
|
||||||
>
|
<FormLabel>开发语言</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
placeholder="请选择开发语言"
|
disabled={isEdit}
|
||||||
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>
|
<FormControl>
|
||||||
<Select.Option value={DevelopmentLanguageTypeEnum.PYTHON}>Python</Select.Option>
|
<SelectTrigger>
|
||||||
<Select.Option value={DevelopmentLanguageTypeEnum.GO}>Go</Select.Option>
|
<SelectValue placeholder="请选择开发语言"/>
|
||||||
</Select>
|
</SelectTrigger>
|
||||||
</Form.Item>
|
</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>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Item
|
<FormField
|
||||||
name="repoUrl"
|
control={form.control}
|
||||||
label="仓库地址"
|
name="repoUrl"
|
||||||
rules={[
|
render={({field}) => (
|
||||||
{required: true, message: '请输入仓库地址'},
|
<FormItem>
|
||||||
{type: 'url', message: '请输入有效的URL地址'},
|
<FormLabel>仓库地址</FormLabel>
|
||||||
]}
|
<FormControl>
|
||||||
tooltip="应用代码仓库的URL地址"
|
<Input
|
||||||
>
|
{...field}
|
||||||
<Input
|
placeholder="请输入仓库地址"
|
||||||
placeholder="请输入仓库地址"
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</Form.Item>
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Item
|
<FormField
|
||||||
name="appDesc"
|
control={form.control}
|
||||||
label="应用描述"
|
name="appDesc"
|
||||||
rules={[{max: 200, message: '应用描述不能超过200个字符'}]}
|
render={({field}) => (
|
||||||
tooltip="应用的详细描述信息"
|
<FormItem>
|
||||||
>
|
<FormLabel>应用描述</FormLabel>
|
||||||
<Input.TextArea
|
<FormControl>
|
||||||
placeholder="请输入应用描述"
|
<Textarea
|
||||||
maxLength={200}
|
{...field}
|
||||||
showCount
|
placeholder="请输入应用描述"
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Item
|
<FormField
|
||||||
name="enabled"
|
control={form.control}
|
||||||
label="状态"
|
name="enabled"
|
||||||
valuePropName="checked"
|
render={({field}) => (
|
||||||
tooltip="是否启用该应用"
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
>
|
<div className="space-y-0.5">
|
||||||
<Switch checkedChildren="启用" unCheckedChildren="禁用"/>
|
<FormLabel>状态</FormLabel>
|
||||||
</Form.Item>
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Item
|
<FormField
|
||||||
name="sort"
|
control={form.control}
|
||||||
label="排序"
|
name="sort"
|
||||||
tooltip="数字越小越靠前"
|
render={({field}) => (
|
||||||
>
|
<FormItem>
|
||||||
<InputNumber min={0} style={{width: '100%'}}/>
|
<FormLabel>排序</FormLabel>
|
||||||
</Form.Item>
|
<FormControl>
|
||||||
</Form>
|
<Input
|
||||||
</Modal>
|
{...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>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, {useState, useEffect} from 'react';
|
import React, {useState, useEffect} from 'react';
|
||||||
import {PageContainer} from '@ant-design/pro-layout';
|
import {PageContainer} from '@/components/ui/page-container';
|
||||||
import {Button, Space, Popconfirm, Tag, Select, App} from 'antd';
|
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@ -19,18 +18,79 @@ import type {ProjectGroup} from '../../ProjectGroup/List/types';
|
|||||||
import {ProjectGroupTypeEnum} from '../../ProjectGroup/List/types';
|
import {ProjectGroupTypeEnum} from '../../ProjectGroup/List/types';
|
||||||
import {getProjectTypeInfo} from '../../ProjectGroup/List/utils';
|
import {getProjectTypeInfo} from '../../ProjectGroup/List/utils';
|
||||||
import ApplicationModal from './components/ApplicationModal';
|
import ApplicationModal from './components/ApplicationModal';
|
||||||
import {ProTable} from '@ant-design/pro-components';
|
import {
|
||||||
import type {ProColumns, ActionType} from '@ant-design/pro-components';
|
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 ApplicationList: React.FC = () => {
|
||||||
const [projectGroups, setProjects] = useState<ProjectGroup[]>([]);
|
const [projectGroups, setProjects] = useState<ProjectGroup[]>([]);
|
||||||
const [selectedProjectGroupId, setSelectedProjectGroupId] = useState<number>();
|
const [selectedProjectGroupId, setSelectedProjectGroupId] = useState<number>();
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [currentApplication, setCurrentApplication] = useState<Application>();
|
const [currentApplication, setCurrentApplication] = useState<Application>();
|
||||||
const actionRef = React.useRef<ActionType>();
|
const [list, setList] = useState<Application[]>([]);
|
||||||
const {message: messageApi} = App.useApp();
|
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 () => {
|
const fetchProjects = async () => {
|
||||||
@ -41,7 +101,11 @@ const ApplicationList: React.FC = () => {
|
|||||||
setSelectedProjectGroupId(data[0].id);
|
setSelectedProjectGroupId(data[0].id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
messageApi.error('获取项目组列表失败');
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "获取项目组列表失败",
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -49,19 +113,58 @@ const ApplicationList: React.FC = () => {
|
|||||||
fetchProjects();
|
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) => {
|
const handleDelete = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await deleteApplication(id);
|
await deleteApplication(id);
|
||||||
messageApi.success('删除成功');
|
toast({
|
||||||
actionRef.current?.reload();
|
title: "删除成功",
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
loadData(form.getValues());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
messageApi.error('删除失败');
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "删除失败",
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
if (!selectedProjectGroupId) {
|
if (!selectedProjectGroupId) {
|
||||||
messageApi.warning('请先选择项目组');
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "请先选择项目组",
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCurrentApplication(undefined);
|
setCurrentApplication(undefined);
|
||||||
@ -73,9 +176,8 @@ const ApplicationList: React.FC = () => {
|
|||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProjectChange = (value: number) => {
|
const handleProjectChange = (value: string) => {
|
||||||
setSelectedProjectGroupId(value);
|
setSelectedProjectGroupId(Number(value));
|
||||||
actionRef.current?.reload();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModalClose = () => {
|
const handleModalClose = () => {
|
||||||
@ -86,7 +188,7 @@ const ApplicationList: React.FC = () => {
|
|||||||
const handleSuccess = () => {
|
const handleSuccess = () => {
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setCurrentApplication(undefined);
|
setCurrentApplication(undefined);
|
||||||
actionRef.current?.reload();
|
loadData(form.getValues());
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取开发语言信息
|
// 获取开发语言信息
|
||||||
@ -125,217 +227,242 @@ const ApplicationList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ProColumns<Application>[] = [
|
const columns: Column[] = [
|
||||||
{
|
{
|
||||||
title: '应用编码',
|
accessorKey: 'appCode',
|
||||||
dataIndex: 'appCode',
|
header: '应用编码',
|
||||||
width: 180,
|
size: 180,
|
||||||
copyable: true,
|
|
||||||
ellipsis: true,
|
|
||||||
fixed: 'left',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '应用名称',
|
accessorKey: 'appName',
|
||||||
dataIndex: 'appName',
|
header: '应用名称',
|
||||||
width: 150,
|
size: 150,
|
||||||
ellipsis: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '项目组',
|
id: 'projectGroup',
|
||||||
dataIndex: ['projectGroup', 'projectGroupName'],
|
header: '项目组',
|
||||||
width: 150,
|
size: 150,
|
||||||
ellipsis: true,
|
cell: ({row}) => (
|
||||||
render: (_, record) => (
|
<div className="flex items-center gap-2">
|
||||||
<Space>
|
<span>{row.original.projectGroup?.projectGroupName}</span>
|
||||||
{record.projectGroup?.projectGroupName}
|
{row.original.projectGroup?.type && (
|
||||||
{record.projectGroup?.type && (
|
<Badge variant="outline">
|
||||||
<Tag color={getProjectTypeInfo(record.projectGroup.type).color}>
|
{getProjectTypeInfo(row.original.projectGroup.type).label}
|
||||||
{getProjectTypeInfo(record.projectGroup.type).label}
|
</Badge>
|
||||||
</Tag>
|
|
||||||
)}
|
)}
|
||||||
</Space>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '应用描述',
|
accessorKey: 'appDesc',
|
||||||
dataIndex: 'appDesc',
|
header: '应用描述',
|
||||||
ellipsis: true,
|
size: 200,
|
||||||
width: 200,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '仓库地址',
|
accessorKey: 'repoUrl',
|
||||||
dataIndex: 'repoUrl',
|
header: '仓库地址',
|
||||||
width: 200,
|
size: 200,
|
||||||
ellipsis: true,
|
cell: ({row}) => row.original.repoUrl ? (
|
||||||
render: (_, record) => record.repoUrl ? (
|
<div className="flex items-center gap-2">
|
||||||
<Space>
|
|
||||||
<GithubOutlined/>
|
<GithubOutlined/>
|
||||||
<a href={record.repoUrl} target="_blank" rel="noopener noreferrer">
|
<a href={row.original.repoUrl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:text-blue-700">
|
||||||
{record.repoUrl}
|
{row.original.repoUrl}
|
||||||
</a>
|
</a>
|
||||||
</Space>
|
</div>
|
||||||
) : '-',
|
) : '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '开发语言',
|
accessorKey: 'language',
|
||||||
dataIndex: 'language',
|
header: '开发语言',
|
||||||
width: 120,
|
size: 120,
|
||||||
render: (language) => {
|
cell: ({row}) => {
|
||||||
const langInfo = getLanguageInfo(language as DevelopmentLanguageTypeEnum);
|
const langInfo = getLanguageInfo(row.original.language);
|
||||||
return (
|
return (
|
||||||
<Tag color={langInfo.color}>
|
<Badge variant="outline" className="flex items-center gap-1">
|
||||||
<Space>
|
{langInfo.icon}
|
||||||
{langInfo.icon}
|
{langInfo.label}
|
||||||
{langInfo.label}
|
</Badge>
|
||||||
</Space>
|
|
||||||
</Tag>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
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: '状态',
|
accessorKey: 'enabled',
|
||||||
dataIndex: 'enabled',
|
header: '状态',
|
||||||
width: 100,
|
size: 100,
|
||||||
valueEnum: {
|
cell: ({row}) => (
|
||||||
true: {text: '启用', status: 'Success'},
|
<Badge variant={row.original.enabled ? "outline" : "secondary"}>
|
||||||
false: {text: '禁用', status: 'Default'},
|
{row.original.enabled ? '启用' : '禁用'}
|
||||||
},
|
</Badge>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '排序',
|
accessorKey: 'sort',
|
||||||
dataIndex: 'sort',
|
header: '排序',
|
||||||
width: 80,
|
size: 80,
|
||||||
sorter: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
id: 'actions',
|
||||||
width: 180,
|
header: '操作',
|
||||||
key: 'action',
|
size: 180,
|
||||||
valueType: 'option',
|
cell: ({row}) => (
|
||||||
fixed: 'right',
|
<div className="flex items-center gap-2">
|
||||||
render: (_, record) => [
|
|
||||||
<Button
|
|
||||||
key="edit"
|
|
||||||
type="link"
|
|
||||||
onClick={() => handleEdit(record)}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
<EditOutlined/>
|
|
||||||
编辑
|
|
||||||
</Space>
|
|
||||||
</Button>,
|
|
||||||
<Popconfirm
|
|
||||||
key="delete"
|
|
||||||
title="确定要删除该应用吗?"
|
|
||||||
description="删除后将无法恢复,请谨慎操作"
|
|
||||||
onConfirm={() => handleDelete(record.id)}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
variant="ghost"
|
||||||
danger
|
size="sm"
|
||||||
|
onClick={() => handleEdit(row.original)}
|
||||||
>
|
>
|
||||||
<Space>
|
<EditOutlined className="mr-1"/>
|
||||||
<DeleteOutlined/>
|
编辑
|
||||||
删除
|
|
||||||
</Space>
|
|
||||||
</Button>
|
</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 (
|
return (
|
||||||
<PageContainer
|
<PageContainer>
|
||||||
header={{
|
<div className="flex items-center justify-between">
|
||||||
title: '应用管理',
|
<h2 className="text-3xl font-bold tracking-tight">应用管理</h2>
|
||||||
extra: [
|
<div className="flex items-center gap-4">
|
||||||
<Select
|
<Select
|
||||||
key="project-select"
|
value={selectedProjectGroupId?.toString()}
|
||||||
value={selectedProjectGroupId}
|
onValueChange={handleProjectChange}
|
||||||
onChange={handleProjectChange}
|
|
||||||
style={{width: 200}}
|
|
||||||
placeholder="请选择项目组"
|
|
||||||
>
|
>
|
||||||
{projectGroups.map((project) => (
|
<SelectTrigger className="w-[200px]">
|
||||||
<Option key={project.id} value={project.id}>
|
<SelectValue placeholder="请选择项目组"/>
|
||||||
{project.projectGroupName}
|
</SelectTrigger>
|
||||||
</Option>
|
<SelectContent>
|
||||||
))}
|
{projectGroups.map((project) => (
|
||||||
</Select>,
|
<SelectItem key={project.id} value={project.id.toString()}>
|
||||||
],
|
{project.projectGroupName}
|
||||||
}}
|
</SelectItem>
|
||||||
>
|
))}
|
||||||
<ProTable<Application>
|
</SelectContent>
|
||||||
columns={columns}
|
</Select>
|
||||||
actionRef={actionRef}
|
<Button onClick={handleAdd} disabled={!selectedProjectGroupId}>
|
||||||
scroll={{x: 'max-content'}}
|
<PlusOutlined className="mr-1"/>
|
||||||
cardBordered
|
新建
|
||||||
rowKey="id"
|
</Button>
|
||||||
search={false}
|
</div>
|
||||||
options={{
|
</div>
|
||||||
setting: false,
|
|
||||||
density: false,
|
<Card>
|
||||||
fullScreen: false,
|
<div className="p-6">
|
||||||
reload: false,
|
<div className="flex items-center gap-4">
|
||||||
}}
|
<Input
|
||||||
toolbar={{
|
placeholder="应用编码"
|
||||||
actions: [
|
value={form.getValues('appCode')}
|
||||||
<Button
|
onChange={(e) => form.setValue('appCode', e.target.value)}
|
||||||
key="add"
|
className="max-w-[200px]"
|
||||||
type="primary"
|
/>
|
||||||
onClick={handleAdd}
|
<Input
|
||||||
icon={<PlusOutlined/>}
|
placeholder="应用名称"
|
||||||
disabled={!selectedProjectGroupId}
|
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>
|
||||||
],
|
<Button variant="ghost" onClick={() => loadData(form.getValues())}>
|
||||||
}}
|
搜索
|
||||||
pagination={{
|
</Button>
|
||||||
pageSize: 10,
|
</div>
|
||||||
showQuickJumper: true,
|
</div>
|
||||||
}}
|
</Card>
|
||||||
request={async (params) => {
|
|
||||||
if (!selectedProjectGroupId) {
|
<Card>
|
||||||
return {
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
data: [],
|
<CardTitle>应用列表</CardTitle>
|
||||||
success: true,
|
</CardHeader>
|
||||||
total: 0,
|
<CardContent>
|
||||||
};
|
<div className="rounded-md border">
|
||||||
}
|
<Table>
|
||||||
try {
|
<TableHeader>
|
||||||
const queryParams: ApplicationQuery = {
|
<TableRow>
|
||||||
pageSize: params.pageSize,
|
{columns.map((column) => (
|
||||||
pageNum: params.current,
|
<TableHead
|
||||||
projectGroupId: selectedProjectGroupId,
|
key={column.accessorKey || column.id}
|
||||||
appCode: params.appCode as string,
|
style={{width: column.size}}
|
||||||
appName: params.appName as string,
|
>
|
||||||
enabled: params.enabled as boolean,
|
{column.header}
|
||||||
};
|
</TableHead>
|
||||||
const data = await getApplicationPage(queryParams);
|
))}
|
||||||
return {
|
</TableRow>
|
||||||
data: data.content || [],
|
</TableHeader>
|
||||||
success: true,
|
<TableBody>
|
||||||
total: data.totalElements || 0,
|
{list.map((item) => (
|
||||||
};
|
<TableRow key={item.id}>
|
||||||
} catch (error) {
|
{columns.map((column) => (
|
||||||
messageApi.error('获取应用列表失败');
|
<TableCell
|
||||||
return {
|
key={column.accessorKey || column.id}
|
||||||
data: [],
|
>
|
||||||
success: false,
|
{column.cell
|
||||||
total: 0,
|
? column.cell({row: {original: item}})
|
||||||
};
|
: item[column.accessorKey!]}
|
||||||
}
|
</TableCell>
|
||||||
}}
|
))}
|
||||||
/>
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{modalVisible && selectedProjectGroupId && (
|
{modalVisible && selectedProjectGroupId && (
|
||||||
<ApplicationModal
|
<ApplicationModal
|
||||||
|
|||||||
24
frontend/src/pages/Deploy/Application/List/schema.ts
Normal file
24
frontend/src/pages/Deploy/Application/List/schema.ts
Normal 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>;
|
||||||
Loading…
Reference in New Issue
Block a user