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 {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
|
||||
name="appCode"
|
||||
label="应用编码"
|
||||
rules={[
|
||||
{required: true, message: '请输入应用编码'},
|
||||
{max: 50, message: '应用编码不能超过50个字符'},
|
||||
]}
|
||||
tooltip="应用的唯一标识,创建后不可修改"
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入应用编码"
|
||||
disabled={isEdit}
|
||||
maxLength={50}
|
||||
/>
|
||||
</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"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>应用编码</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isEdit}
|
||||
placeholder="请输入应用编码"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="appName"
|
||||
label="应用名称"
|
||||
rules={[
|
||||
{required: true, message: '请输入应用名称'},
|
||||
{max: 50, message: '应用名称不能超过50个字符'},
|
||||
]}
|
||||
tooltip="应用的显示名称"
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入应用名称"
|
||||
maxLength={50}
|
||||
/>
|
||||
</Form.Item>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>应用名称</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="请输入应用名称"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="language"
|
||||
label="开发语言"
|
||||
rules={[{required: true, message: '请选择开发语言'}]}
|
||||
tooltip="应用的主要开发语言,创建后不可修改"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择开发语言"
|
||||
disabled={isEdit}
|
||||
>
|
||||
<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>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="language"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>开发语言</FormLabel>
|
||||
<Select
|
||||
disabled={isEdit}
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<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>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="repoUrl"
|
||||
label="仓库地址"
|
||||
rules={[
|
||||
{required: true, message: '请输入仓库地址'},
|
||||
{type: 'url', message: '请输入有效的URL地址'},
|
||||
]}
|
||||
tooltip="应用代码仓库的URL地址"
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入仓库地址"
|
||||
/>
|
||||
</Form.Item>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repoUrl"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>仓库地址</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="请输入仓库地址"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="appDesc"
|
||||
label="应用描述"
|
||||
rules={[{max: 200, message: '应用描述不能超过200个字符'}]}
|
||||
tooltip="应用的详细描述信息"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入应用描述"
|
||||
maxLength={200}
|
||||
showCount
|
||||
rows={4}
|
||||
/>
|
||||
</Form.Item>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appDesc"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>应用描述</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder="请输入应用描述"
|
||||
rows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label="状态"
|
||||
valuePropName="checked"
|
||||
tooltip="是否启用该应用"
|
||||
>
|
||||
<Switch checkedChildren="启用" unCheckedChildren="禁用"/>
|
||||
</Form.Item>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
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
|
||||
name="sort"
|
||||
label="排序"
|
||||
tooltip="数字越小越靠前"
|
||||
>
|
||||
<InputNumber min={0} style={{width: '100%'}}/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sort"
|
||||
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>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
{langInfo.icon}
|
||||
{langInfo.label}
|
||||
</Space>
|
||||
</Tag>
|
||||
<Badge variant="outline" className="flex items-center gap-1">
|
||||
{langInfo.icon}
|
||||
{langInfo.label}
|
||||
</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) => [
|
||||
<Button
|
||||
key="edit"
|
||||
type="link"
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
<Space>
|
||||
<EditOutlined/>
|
||||
编辑
|
||||
</Space>
|
||||
</Button>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="确定要删除该应用吗?"
|
||||
description="删除后将无法恢复,请谨慎操作"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
id: 'actions',
|
||||
header: '操作',
|
||||
size: 180,
|
||||
cell: ({row}) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
>
|
||||
<Space>
|
||||
<DeleteOutlined/>
|
||||
删除
|
||||
</Space>
|
||||
<EditOutlined className="mr-1"/>
|
||||
编辑
|
||||
</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}
|
||||
>
|
||||
{projectGroups.map((project) => (
|
||||
<Option key={project.id} value={project.id}>
|
||||
{project.projectGroupName}
|
||||
</Option>
|
||||
))}
|
||||
</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}
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="请选择项目组"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projectGroups.map((project) => (
|
||||
<SelectItem key={project.id} value={project.id.toString()}>
|
||||
{project.projectGroupName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={handleAdd} disabled={!selectedProjectGroupId}>
|
||||
<PlusOutlined className="mr-1"/>
|
||||
新建
|
||||
</Button>
|
||||
</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>
|
||||
],
|
||||
}}
|
||||
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,
|
||||
};
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
|
||||
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