增加应用分类
This commit is contained in:
parent
93551ef55f
commit
d499d6b759
46
frontend/src/pages/Deploy/Application/Category/service.ts
Normal file
46
frontend/src/pages/Deploy/Application/Category/service.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import request from '@/utils/request';
|
||||
import type { Page } from '@/types/base';
|
||||
import type {
|
||||
ApplicationCategoryQuery,
|
||||
ApplicationCategoryResponse,
|
||||
ApplicationCategoryRequest,
|
||||
} from './types';
|
||||
|
||||
const BASE_URL = '/api/v1/application-category';
|
||||
|
||||
/**
|
||||
* 分页查询分类
|
||||
*/
|
||||
export const getCategories = (params?: ApplicationCategoryQuery) =>
|
||||
request.get<Page<ApplicationCategoryResponse>>(`${BASE_URL}/page`, { params });
|
||||
|
||||
/**
|
||||
* 查询分类详情
|
||||
*/
|
||||
export const getCategoryById = (id: number) =>
|
||||
request.get<ApplicationCategoryResponse>(`${BASE_URL}/${id}`);
|
||||
|
||||
/**
|
||||
* 查询所有启用的分类(常用)
|
||||
*/
|
||||
export const getEnabledCategories = () =>
|
||||
request.get<ApplicationCategoryResponse[]>(`${BASE_URL}/enabled`);
|
||||
|
||||
/**
|
||||
* 创建分类
|
||||
*/
|
||||
export const createCategory = (data: ApplicationCategoryRequest) =>
|
||||
request.post<ApplicationCategoryResponse>(BASE_URL, data);
|
||||
|
||||
/**
|
||||
* 更新分类
|
||||
*/
|
||||
export const updateCategory = (id: number, data: ApplicationCategoryRequest) =>
|
||||
request.put<ApplicationCategoryResponse>(`${BASE_URL}/${id}`, data);
|
||||
|
||||
/**
|
||||
* 删除分类
|
||||
*/
|
||||
export const deleteCategory = (id: number) =>
|
||||
request.delete(`${BASE_URL}/${id}`);
|
||||
|
||||
40
frontend/src/pages/Deploy/Application/Category/types.ts
Normal file
40
frontend/src/pages/Deploy/Application/Category/types.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { BaseQuery } from '@/types/base';
|
||||
|
||||
/**
|
||||
* 应用分类查询参数
|
||||
*/
|
||||
export interface ApplicationCategoryQuery extends BaseQuery {
|
||||
name?: string;
|
||||
code?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用分类响应
|
||||
*/
|
||||
export interface ApplicationCategoryResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
sort: number;
|
||||
enabled: boolean;
|
||||
createBy?: string;
|
||||
createTime?: string;
|
||||
updateBy?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用分类创建/更新请求
|
||||
*/
|
||||
export interface ApplicationCategoryRequest {
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
sort?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import {createApplication, updateApplication, getRepositoryGroupList, getReposit
|
||||
import type {ExternalSystemResponse} from '@/pages/Deploy/External/types';
|
||||
import {SystemType} from '@/pages/Deploy/External/types';
|
||||
import {getExternalSystems} from '@/pages/Deploy/External/service';
|
||||
import {getEnabledCategories} from '../../Category/service';
|
||||
import type {ApplicationCategoryResponse} from '../../Category/types';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -35,7 +37,6 @@ import {useForm} from "react-hook-form";
|
||||
import {zodResolver} from "@hookform/resolvers/zod";
|
||||
import {applicationFormSchema, type ApplicationFormValues} from "../schema";
|
||||
import {Textarea} from "@/components/ui/textarea";
|
||||
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem} from "@/components/ui/command";
|
||||
import {ScrollArea} from "@/components/ui/scroll-area";
|
||||
import {Check, ChevronDown, Search} from "lucide-react";
|
||||
import {cn} from "@/lib/utils";
|
||||
@ -46,7 +47,6 @@ interface ApplicationModalProps {
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
initialValues?: Application;
|
||||
projectGroupId: number;
|
||||
}
|
||||
|
||||
const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
@ -54,14 +54,13 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
onCancel,
|
||||
onSuccess,
|
||||
initialValues,
|
||||
projectGroupId,
|
||||
}) => {
|
||||
const [categories, setCategories] = useState<ApplicationCategoryResponse[]>([]);
|
||||
const [externalSystems, setExternalSystems] = useState<ExternalSystemResponse[]>([]);
|
||||
const [repositoryGroups, setRepositoryGroups] = useState<RepositoryGroup[]>([]);
|
||||
const [repositoryProjects, setRepositoryProjects] = useState<RepositoryProject[]>([]);
|
||||
const {toast} = useToast();
|
||||
const isEdit = !!initialValues?.id;
|
||||
const [selectedExternalSystem, setSelectedExternalSystem] = useState<ExternalSystemResponse>();
|
||||
|
||||
const form = useForm<ApplicationFormValues>({
|
||||
resolver: zodResolver(applicationFormSchema),
|
||||
@ -69,16 +68,31 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
appCode: "",
|
||||
appName: "",
|
||||
appDesc: "",
|
||||
categoryId: undefined,
|
||||
language: DevelopmentLanguageTypeEnum.JAVA,
|
||||
enabled: true,
|
||||
sort: 0,
|
||||
externalSystemId: undefined,
|
||||
repoGroupId: undefined,
|
||||
repoProjectId: undefined,
|
||||
projectGroupId,
|
||||
},
|
||||
});
|
||||
|
||||
// 加载分类列表
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const data = await getEnabledCategories();
|
||||
setCategories(data || []);
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "加载分类失败",
|
||||
description: error instanceof Error ? error.message : undefined,
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 加载外部系统列表
|
||||
const loadExternalSystems = async () => {
|
||||
try {
|
||||
@ -100,6 +114,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadCategories();
|
||||
loadExternalSystems();
|
||||
}, []);
|
||||
|
||||
@ -110,10 +125,10 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
appCode: initialValues.appCode,
|
||||
appName: initialValues.appName,
|
||||
appDesc: initialValues.appDesc || "",
|
||||
categoryId: initialValues.categoryId,
|
||||
language: initialValues.language,
|
||||
enabled: initialValues.enabled,
|
||||
sort: initialValues.sort,
|
||||
projectGroupId: initialValues.projectGroupId,
|
||||
externalSystemId: initialValues.externalSystemId,
|
||||
repoGroupId: initialValues.repoGroupId,
|
||||
repoProjectId: initialValues.repoProjectId
|
||||
@ -132,19 +147,23 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
}, [initialValues]);
|
||||
|
||||
// 当选择外部系统时,获取对应的仓库组列表
|
||||
const handleExternalSystemChange = (externalSystemId: number) => {
|
||||
const handleExternalSystemChange = (externalSystemId: number | undefined) => {
|
||||
form.setValue('repoGroupId', undefined);
|
||||
form.setValue('repoProjectId', undefined);
|
||||
setRepositoryProjects([]);
|
||||
fetchRepositoryGroups(externalSystemId);
|
||||
if (externalSystemId) {
|
||||
fetchRepositoryGroups(externalSystemId);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRepositoryGroups = async (externalSystemId: number) => {
|
||||
const fetchRepositoryGroups = async (externalSystemId: number | undefined) => {
|
||||
if (!externalSystemId) return;
|
||||
const response = await getRepositoryGroupList(externalSystemId);
|
||||
setRepositoryGroups(response || []);
|
||||
};
|
||||
|
||||
const fetchRepositoryProjects = async (repoGroupId: number) => {
|
||||
const fetchRepositoryProjects = async (repoGroupId: number | undefined) => {
|
||||
if (!repoGroupId) return;
|
||||
const response = await getRepositoryProjects(repoGroupId);
|
||||
setRepositoryProjects(response || []);
|
||||
};
|
||||
@ -156,14 +175,14 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
await updateApplication({
|
||||
...values,
|
||||
id: initialValues!.id,
|
||||
});
|
||||
} as any);
|
||||
toast({
|
||||
title: "更新成功",
|
||||
description: `应用 ${values.appName} 已更新`,
|
||||
duration: 3000,
|
||||
});
|
||||
} else {
|
||||
await createApplication(values);
|
||||
await createApplication(values as any);
|
||||
toast({
|
||||
title: "创建成功",
|
||||
description: `应用 ${values.appName} 已创建`,
|
||||
@ -229,6 +248,36 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="categoryId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>应用分类</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(Number(value))}
|
||||
value={field.value?.toString()}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择应用分类"/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category.id} value={category.id.toString()}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="language"
|
||||
@ -354,11 +403,12 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground",
|
||||
group.repoGroupId === field.value && "bg-accent text-accent-foreground"
|
||||
)}
|
||||
onClick={() => {
|
||||
form.setValue("repoGroupId", group.repoGroupId);
|
||||
form.setValue("repoProjectId", undefined);
|
||||
setRepositoryProjects([]);
|
||||
fetchRepositoryProjects(group.repoGroupId);
|
||||
onClick={() => {
|
||||
const groupId = group.repoGroupId;
|
||||
form.setValue("repoGroupId", groupId);
|
||||
form.setValue("repoProjectId", undefined as any);
|
||||
setRepositoryProjects([]);
|
||||
fetchRepositoryProjects(groupId);
|
||||
setSearchValue("");
|
||||
setOpen(false);
|
||||
}}
|
||||
@ -547,10 +597,10 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
console.log('Submit button clicked');
|
||||
console.log('Form values:', form.getValues());
|
||||
const isValid = form.trigger();
|
||||
const isValid = await form.trigger();
|
||||
console.log('Form validation result:', isValid);
|
||||
if (isValid) {
|
||||
handleSubmit(form.getValues());
|
||||
|
||||
@ -0,0 +1,562 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Search,
|
||||
Loader2,
|
||||
FolderKanban,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import DynamicIcon from '@/components/DynamicIcon';
|
||||
import LucideIconSelect from '@/components/LucideIconSelect';
|
||||
import {
|
||||
getCategories,
|
||||
createCategory,
|
||||
updateCategory,
|
||||
deleteCategory
|
||||
} from '../../Category/service';
|
||||
import type {
|
||||
ApplicationCategoryResponse,
|
||||
ApplicationCategoryRequest,
|
||||
ApplicationCategoryQuery
|
||||
} from '../../Category/types';
|
||||
import type { Page } from '@/types/base';
|
||||
|
||||
interface CategoryManageDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用分类管理对话框
|
||||
*/
|
||||
const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<Page<ApplicationCategoryResponse> | null>(null);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editRecord, setEditRecord] = useState<ApplicationCategoryResponse | null>(null);
|
||||
const [iconSelectOpen, setIconSelectOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteRecord, setDeleteRecord] = useState<ApplicationCategoryResponse | null>(null);
|
||||
|
||||
// 分页状态
|
||||
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
|
||||
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
|
||||
const form = useForm<ApplicationCategoryRequest>({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
code: '',
|
||||
icon: '',
|
||||
sort: 0,
|
||||
description: '',
|
||||
enabled: true,
|
||||
}
|
||||
});
|
||||
|
||||
// 加载分类列表
|
||||
const loadCategories = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const query: ApplicationCategoryQuery = {
|
||||
name: searchText || undefined,
|
||||
pageNum,
|
||||
pageSize,
|
||||
};
|
||||
const result = await getCategories(query);
|
||||
setData(result || null);
|
||||
} catch (error) {
|
||||
console.error('加载分类失败:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "加载失败",
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadCategories();
|
||||
}
|
||||
}, [open, searchText, pageNum, pageSize]);
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
setPageNum(0); // 搜索时重置到第一页
|
||||
};
|
||||
|
||||
// 新建
|
||||
const handleCreate = () => {
|
||||
form.reset({
|
||||
name: '',
|
||||
code: '',
|
||||
icon: '',
|
||||
sort: 0,
|
||||
description: '',
|
||||
enabled: true,
|
||||
});
|
||||
setEditRecord(null);
|
||||
setEditMode(true);
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: ApplicationCategoryResponse) => {
|
||||
setEditRecord(record);
|
||||
form.reset({
|
||||
name: record.name,
|
||||
code: record.code,
|
||||
icon: record.icon || '',
|
||||
sort: record.sort,
|
||||
description: record.description || '',
|
||||
enabled: record.enabled,
|
||||
});
|
||||
setEditMode(true);
|
||||
};
|
||||
|
||||
// 打开删除确认对话框
|
||||
const handleDeleteClick = (record: ApplicationCategoryResponse) => {
|
||||
setDeleteRecord(record);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = async () => {
|
||||
if (!deleteRecord) return;
|
||||
|
||||
try {
|
||||
await deleteCategory(deleteRecord.id);
|
||||
toast({
|
||||
title: "删除成功",
|
||||
description: `分类 "${deleteRecord.name}" 已删除`,
|
||||
});
|
||||
loadCategories();
|
||||
onSuccess?.();
|
||||
setDeleteDialogOpen(false);
|
||||
setDeleteRecord(null);
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "删除失败",
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 保存
|
||||
const handleSave = async (values: ApplicationCategoryRequest) => {
|
||||
try {
|
||||
if (editRecord) {
|
||||
await updateCategory(editRecord.id, values);
|
||||
toast({
|
||||
title: "更新成功",
|
||||
description: `分类 "${values.name}" 已更新`,
|
||||
});
|
||||
} else {
|
||||
await createCategory(values);
|
||||
toast({
|
||||
title: "创建成功",
|
||||
description: `分类 "${values.name}" 已创建`,
|
||||
});
|
||||
}
|
||||
setEditMode(false);
|
||||
loadCategories();
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "保存失败",
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const handleCancel = () => {
|
||||
setEditMode(false);
|
||||
setEditRecord(null);
|
||||
form.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-6xl max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FolderKanban className="h-5 w-5" />
|
||||
应用分类管理
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{!editMode ? (
|
||||
<div className="space-y-4">
|
||||
{/* 搜索栏 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索分类名称..."
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建分类
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 表格 */}
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[160px]">分类名称</TableHead>
|
||||
<TableHead className="w-[140px]">分类代码</TableHead>
|
||||
<TableHead className="w-[60px]">图标</TableHead>
|
||||
<TableHead className="w-[80px]">排序</TableHead>
|
||||
<TableHead className="w-[80px]">状态</TableHead>
|
||||
<TableHead className="min-w-[150px]">描述</TableHead>
|
||||
<TableHead className="w-[100px] sticky right-0 bg-background shadow-[-2px_0_4px_rgba(0,0,0,0.05)]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-8">
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
<span className="text-sm text-muted-foreground">加载中...</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : data?.content && data.content.length > 0 ? (
|
||||
data.content.map((record) => (
|
||||
<TableRow key={record.id}>
|
||||
<TableCell className="font-medium">
|
||||
{record.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs bg-muted px-2 py-0.5 rounded whitespace-nowrap">
|
||||
{record.code}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{record.icon ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<DynamicIcon name={record.icon} className="h-5 w-5" />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{record.sort}</TableCell>
|
||||
<TableCell>
|
||||
{record.enabled ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[200px] truncate" title={record.description}>
|
||||
{record.description || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="sticky right-0 bg-background shadow-[-2px_0_4px_rgba(0,0,0,0.05)]">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
<Edit className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handleDeleteClick(record)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-12 text-muted-foreground">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<FolderKanban className="h-12 w-12 opacity-20" />
|
||||
<p className="text-sm">暂无分类</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{data && data.totalElements > 0 && (
|
||||
<div className="mt-4">
|
||||
<DataTablePagination
|
||||
pageIndex={pageNum + 1}
|
||||
pageSize={pageSize}
|
||||
pageCount={Math.ceil((data.totalElements || 0) / pageSize)}
|
||||
onPageChange={(page) => setPageNum(page - 1)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSave)} className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
rules={{ required: '请输入分类名称' }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>分类名称</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="输入分类名称" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="code"
|
||||
rules={{ required: '请输入分类代码' }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>分类代码</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="输入分类代码" {...field} disabled={!!editRecord} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="icon"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>图标</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder="点击选择图标"
|
||||
value={field.value}
|
||||
readOnly
|
||||
onClick={() => setIconSelectOpen(true)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
{field.value && (
|
||||
<div className="flex items-center justify-center w-10 h-10 border rounded-md">
|
||||
<DynamicIcon name={field.value} className="h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<LucideIconSelect
|
||||
open={iconSelectOpen}
|
||||
onOpenChange={setIconSelectOpen}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>排序</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="输入排序值"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>描述</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="输入分类描述"
|
||||
className="resize-none"
|
||||
rows={3}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">启用状态</FormLabel>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
控制此分类是否可用
|
||||
</div>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除分类</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-3">
|
||||
<p>您确定要删除以下分类吗?此操作无法撤销。</p>
|
||||
{deleteRecord && (
|
||||
<div className="rounded-md border p-3 space-y-2 bg-muted/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">分类名称:</span>
|
||||
<span className="font-medium">{deleteRecord.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">分类代码:</span>
|
||||
<code className="text-xs bg-background px-2 py-1 rounded">
|
||||
{deleteRecord.code}
|
||||
</code>
|
||||
</div>
|
||||
{deleteRecord.icon && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">图标:</span>
|
||||
<DynamicIcon name={deleteRecord.icon} className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
{deleteRecord.description && (
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">描述:</span>
|
||||
<span className="text-sm">{deleteRecord.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
确认删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryManageDialog;
|
||||
|
||||
@ -81,14 +81,22 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
||||
<span className="text-muted-foreground">应用名称:</span>
|
||||
<span className="font-medium">{record.appName}</span>
|
||||
</div>
|
||||
{record.category && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">应用分类:</span>
|
||||
<span className="font-medium">{record.category.name}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">开发语言:</span>
|
||||
<Badge variant="outline">{getLanguageLabel(record.language)}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">项目组:</span>
|
||||
<span className="font-medium">{record.projectGroup?.projectGroupName || '-'}</span>
|
||||
</div>
|
||||
{record.appDesc && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">应用描述:</span>
|
||||
<span className="font-medium">{record.appDesc}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@ -1,22 +1,20 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { PageContainer } from '@/components/ui/page-container';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CodeOutlined,
|
||||
GithubOutlined,
|
||||
JavaOutlined,
|
||||
NodeIndexOutlined,
|
||||
PythonOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getApplicationPage, deleteApplication } from './service';
|
||||
import { getProjectGroupList } from '../../ProjectGroup/List/service';
|
||||
import { getApplicationPage } from './service';
|
||||
import type { Application, ApplicationQuery } from './types';
|
||||
import { DevelopmentLanguageTypeEnum } from './types';
|
||||
import type { ProjectGroup } from '../../ProjectGroup/List/types';
|
||||
import { getEnabledCategories } from '../Category/service';
|
||||
import type { ApplicationCategoryResponse } from '../Category/types';
|
||||
import ApplicationModal from './components/ApplicationModal';
|
||||
import DeleteDialog from './components/DeleteDialog';
|
||||
import CategoryManageDialog from './components/CategoryManageDialog';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
@ -30,7 +28,9 @@ import {
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
@ -46,7 +46,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { searchFormSchema, type SearchFormValues } from './schema';
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { Plus, Edit, Trash2, Server, Activity, Database, ExternalLink } from 'lucide-react';
|
||||
import { Plus, Edit, Trash2, Server, Activity, Database, ExternalLink, Settings } from 'lucide-react';
|
||||
|
||||
interface Column {
|
||||
accessorKey?: keyof Application;
|
||||
@ -59,13 +59,14 @@ interface Column {
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
const ApplicationList: React.FC = () => {
|
||||
const [projectGroups, setProjects] = useState<ProjectGroup[]>([]);
|
||||
const [selectedProjectGroupId, setSelectedProjectGroupId] = useState<number>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
|
||||
const [currentApplication, setCurrentApplication] = useState<Application>();
|
||||
const [list, setList] = useState<Application[]>([]);
|
||||
const [categories, setCategories] = useState<ApplicationCategoryResponse[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<number>();
|
||||
const [pagination, setPagination] = useState({
|
||||
pageNum: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
@ -84,37 +85,26 @@ const ApplicationList: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
// 加载分类列表
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const data = await getProjectGroupList();
|
||||
setProjects(data);
|
||||
if (data.length > 0 && !selectedProjectGroupId) {
|
||||
setSelectedProjectGroupId(data[0].id);
|
||||
}
|
||||
const data = await getEnabledCategories();
|
||||
setCategories(data || []);
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '获取项目组列表失败',
|
||||
duration: 3000,
|
||||
});
|
||||
console.error('加载分类失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
loadCategories();
|
||||
}, []);
|
||||
|
||||
const loadData = async (params?: ApplicationQuery) => {
|
||||
if (!selectedProjectGroupId) {
|
||||
setList([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const queryParams: ApplicationQuery = {
|
||||
...params,
|
||||
projectGroupId: selectedProjectGroupId,
|
||||
categoryId: selectedCategoryId,
|
||||
pageNum: pagination.pageNum,
|
||||
pageSize: pagination.pageSize,
|
||||
};
|
||||
@ -151,17 +141,9 @@ const ApplicationList: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
loadData(form.getValues());
|
||||
}, [selectedProjectGroupId, pagination.pageNum, pagination.pageSize]);
|
||||
}, [selectedCategoryId, pagination.pageNum, pagination.pageSize]);
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!selectedProjectGroupId) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '请先选择项目组',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setCurrentApplication(undefined);
|
||||
setModalVisible(true);
|
||||
};
|
||||
@ -171,10 +153,6 @@ const ApplicationList: React.FC = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleProjectChange = (value: string) => {
|
||||
setSelectedProjectGroupId(Number(value));
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setModalVisible(false);
|
||||
setCurrentApplication(undefined);
|
||||
@ -226,27 +204,27 @@ const ApplicationList: React.FC = () => {
|
||||
{
|
||||
accessorKey: 'appCode',
|
||||
header: '应用编码',
|
||||
size: 180,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
accessorKey: 'appName',
|
||||
header: '应用名称',
|
||||
size: 150,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
id: 'projectGroup',
|
||||
header: '项目组',
|
||||
size: 150,
|
||||
id: 'category',
|
||||
header: '应用分类',
|
||||
size: 120,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{row.original.projectGroup?.projectGroupName}</span>
|
||||
<span>{row.original.category?.name || '-'}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'appDesc',
|
||||
header: '应用描述',
|
||||
size: 200,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
id: 'externalSystem',
|
||||
@ -340,32 +318,6 @@ const ApplicationList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<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
|
||||
value={selectedProjectGroupId?.toString()}
|
||||
onValueChange={handleProjectChange}
|
||||
>
|
||||
<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}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建应用
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
@ -400,10 +352,32 @@ const ApplicationList: React.FC = () => {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 搜索过滤 */}
|
||||
{/* 应用管理 */}
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>应用管理</CardTitle>
|
||||
<CardDescription className="mt-2">
|
||||
创建和管理应用,支持Git集成和分类管理
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
分类管理
|
||||
</Button>
|
||||
<Button onClick={handleAdd}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建应用
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Separator />
|
||||
<CardContent className="pt-6">
|
||||
{/* 搜索过滤 */}
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Input
|
||||
placeholder="应用编码"
|
||||
value={form.watch('appCode')}
|
||||
@ -416,6 +390,29 @@ const ApplicationList: React.FC = () => {
|
||||
onChange={(e) => form.setValue('appName', e.target.value)}
|
||||
className="max-w-[200px]"
|
||||
/>
|
||||
<Select
|
||||
value={selectedCategoryId?.toString()}
|
||||
onValueChange={(value) => {
|
||||
if (value === "all") {
|
||||
setSelectedCategoryId(undefined);
|
||||
} else {
|
||||
setSelectedCategoryId(Number(value));
|
||||
}
|
||||
setPagination({ ...pagination, pageNum: 1 });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="max-w-[200px]">
|
||||
<SelectValue placeholder="全部分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部分类</SelectItem>
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category.id} value={category.id.toString()}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={form.watch('language')}
|
||||
onValueChange={(value) =>
|
||||
@ -449,15 +446,8 @@ const ApplicationList: React.FC = () => {
|
||||
</Button>
|
||||
<Button 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>
|
||||
@ -482,13 +472,22 @@ const ApplicationList: React.FC = () => {
|
||||
) : (
|
||||
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>
|
||||
))}
|
||||
{columns.map((column) => {
|
||||
let cellContent;
|
||||
if (column.cell) {
|
||||
cellContent = column.cell({ row: { original: item } });
|
||||
} else if (column.accessorKey) {
|
||||
const value = item[column.accessorKey];
|
||||
cellContent = value != null ? String(value) : '';
|
||||
} else {
|
||||
cellContent = '';
|
||||
}
|
||||
return (
|
||||
<TableCell key={column.accessorKey || column.id}>
|
||||
{cellContent}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
@ -507,13 +506,12 @@ const ApplicationList: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
{/* 对话框 */}
|
||||
{modalVisible && selectedProjectGroupId && (
|
||||
{modalVisible && (
|
||||
<ApplicationModal
|
||||
open={modalVisible}
|
||||
onCancel={handleModalClose}
|
||||
onSuccess={handleSuccess}
|
||||
initialValues={currentApplication}
|
||||
projectGroupId={selectedProjectGroupId}
|
||||
/>
|
||||
)}
|
||||
<DeleteDialog
|
||||
@ -522,6 +520,11 @@ const ApplicationList: React.FC = () => {
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
<CategoryManageDialog
|
||||
open={categoryDialogOpen}
|
||||
onOpenChange={setCategoryDialogOpen}
|
||||
onSuccess={loadCategories}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -12,19 +12,13 @@ export const applicationFormSchema = z.object({
|
||||
appCode: z.string().min(1, '应用编码不能为空'),
|
||||
appName: z.string().min(1, '应用名称不能为空'),
|
||||
appDesc: z.string().max(200, "应用描述不能超过200个字符").optional(),
|
||||
externalSystemId: z.number({
|
||||
required_error: '请选择Git实例',
|
||||
invalid_type_error: 'Git实例必须是数字',
|
||||
}),
|
||||
projectGroupId: z.number().min(1, "请选择项目组"),
|
||||
repoGroupId: z.number({
|
||||
required_error: '请选择代码仓库组',
|
||||
invalid_type_error: '代码仓库组必须是数字',
|
||||
}),
|
||||
repoProjectId: z.number({
|
||||
required_error: '请选择项目',
|
||||
invalid_type_error: '项目必须是数字',
|
||||
categoryId: z.number({
|
||||
required_error: '请选择应用分类',
|
||||
invalid_type_error: '应用分类必须是数字',
|
||||
}),
|
||||
externalSystemId: z.number().optional(),
|
||||
repoGroupId: z.number().optional(),
|
||||
repoProjectId: z.number().optional(),
|
||||
language: z.nativeEnum(DevelopmentLanguageTypeEnum, {
|
||||
required_error: "请选择开发语言",
|
||||
}),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type {BaseQuery} from '@/types/base';
|
||||
import {ProjectGroup} from "@/pages/Deploy/ProjectGroup/List/types";
|
||||
import {ExternalSystemResponse} from "@/pages/Deploy/External/types";
|
||||
import {BaseResponse} from "@/types/base";
|
||||
import type {ApplicationCategoryResponse} from "../Category/types";
|
||||
|
||||
export enum DevelopmentLanguageTypeEnum {
|
||||
JAVA = 'JAVA',
|
||||
@ -13,17 +13,17 @@ export enum DevelopmentLanguageTypeEnum {
|
||||
export interface Application extends BaseResponse {
|
||||
appCode: string;
|
||||
appName: string;
|
||||
appDesc: string;
|
||||
appDesc?: string;
|
||||
language: DevelopmentLanguageTypeEnum;
|
||||
enabled: boolean;
|
||||
sort: number;
|
||||
projectGroupId: number;
|
||||
projectGroup?: ProjectGroup;
|
||||
externalSystemId: number;
|
||||
categoryId: number;
|
||||
category?: ApplicationCategoryResponse;
|
||||
externalSystemId?: number;
|
||||
externalSystem?: ExternalSystemResponse;
|
||||
repoGroupId: number;
|
||||
repoGroupId?: number;
|
||||
repositoryGroup?: RepositoryGroup;
|
||||
repoProjectId: number;
|
||||
repoProjectId?: number;
|
||||
repositoryProject?: RepositoryProject;
|
||||
}
|
||||
|
||||
@ -40,7 +40,6 @@ export interface RepositoryGroup extends BaseResponse {
|
||||
webUrl?: string;
|
||||
visibility?: string;
|
||||
sort?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface RepositoryProject extends BaseResponse {
|
||||
@ -64,9 +63,9 @@ export type UpdateApplicationRequest = Application;
|
||||
export type ApplicationFormValues = CreateApplicationRequest;
|
||||
|
||||
export interface ApplicationQuery extends BaseQuery {
|
||||
projectGroupId?: number;
|
||||
appCode?: string;
|
||||
appName?: string;
|
||||
categoryId?: number;
|
||||
enabled?: boolean;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
@ -1,109 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { deleteProjectGroup } from '../service';
|
||||
import type { ProjectGroup } from '../types';
|
||||
import { getProjectTypeInfo } from '../utils';
|
||||
|
||||
interface DeleteDialogProps {
|
||||
open: boolean;
|
||||
record?: ProjectGroup;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目组删除确认对话框
|
||||
*/
|
||||
const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
||||
open,
|
||||
record,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!record) return;
|
||||
|
||||
try {
|
||||
await deleteProjectGroup(record.id);
|
||||
toast({ title: '删除成功', description: `项目组 "${record.projectGroupName}" 已删除` });
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
toast({
|
||||
title: '删除失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!record) return null;
|
||||
|
||||
const typeInfo = getProjectTypeInfo(record.type);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-destructive">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
确认删除
|
||||
</DialogTitle>
|
||||
<DialogDescription className="space-y-3 pt-4">
|
||||
<p>您确定要删除以下项目组吗?此操作不可撤销。</p>
|
||||
<div className="bg-muted p-3 rounded-md space-y-2 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">项目组编码:</span>
|
||||
<span className="font-medium">{record.projectGroupCode}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">项目组名称:</span>
|
||||
<span className="font-medium">{record.projectGroupName}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">项目组类型:</span>
|
||||
<Badge variant="outline" className="inline-flex items-center gap-1">
|
||||
{typeInfo.icon}
|
||||
{typeInfo.label}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">环境数量:</span>
|
||||
<span className="font-medium">{record.totalEnvironments}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">项目数量:</span>
|
||||
<span className="font-medium">{record.totalApplications}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleConfirm}>
|
||||
确认删除
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteDialog;
|
||||
|
||||
@ -1,293 +0,0 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormDescription,
|
||||
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 type {ProjectGroup} from '../types';
|
||||
import {ProjectGroupTypeEnum} from '../types';
|
||||
import {createProjectGroup, updateProjectGroup} from '../service';
|
||||
import {formSchema, type FormValues} from '../schema';
|
||||
|
||||
interface ProjectGroupModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
initialValues?: ProjectGroup;
|
||||
}
|
||||
|
||||
const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({
|
||||
open,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
initialValues,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {toast} = useToast();
|
||||
const isEdit = !!initialValues?.id;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
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) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (isEdit) {
|
||||
await updateProjectGroup({
|
||||
...values,
|
||||
id: initialValues!.id,
|
||||
});
|
||||
toast({
|
||||
title: "操作成功",
|
||||
description: "项目组更新成功",
|
||||
variant: "default",
|
||||
});
|
||||
onSuccess?.();
|
||||
} else {
|
||||
await createProjectGroup(values);
|
||||
toast({
|
||||
title: "操作成功",
|
||||
description: "项目组创建成功",
|
||||
variant: "default",
|
||||
});
|
||||
onSuccess?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
toast({
|
||||
title: "操作失败",
|
||||
description: error instanceof Error ? error.message : `项目组${isEdit ? '更新' : '创建'}失败`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(open) => !open && onCancel()}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? '编辑' : '新建'}项目组</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit ? '编辑现有项目组信息' : '创建一个新的项目组'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectGroupCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>项目组编码</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="请输入项目组编码"
|
||||
disabled={isEdit}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
项目组的唯一标识,创建后不可修改。只能包含小写字母、数字和连字符
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectGroupName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>项目组名称</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="请输入项目组名称"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
项目组的显示名称
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>项目组类型</FormLabel>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
disabled={isEdit}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择项目组类型" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value={ProjectGroupTypeEnum.PRODUCT}>产品型</SelectItem>
|
||||
<SelectItem value={ProjectGroupTypeEnum.PROJECT}>项目型</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
项目组的类型,创建后不可修改
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectGroupDesc"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>项目组描述</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="请输入项目组描述"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
项目组的详细描述信息
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<FormDescription>
|
||||
是否启用该项目组
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>排序</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
数值越小越靠前
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
确定
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectGroupModal;
|
||||
@ -1,346 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { PageContainer } from '@/components/ui/page-container';
|
||||
import { getProjectGroupPage } from './service';
|
||||
import type { ProjectGroup, ProjectGroupQueryParams } from './types';
|
||||
import { ProjectGroupTypeEnum } from './types';
|
||||
import { getProjectTypeInfo } from './utils';
|
||||
import ProjectGroupModal from './components/ProjectGroupModal';
|
||||
import DeleteDialog from './components/DeleteDialog';
|
||||
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 { 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 { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { searchFormSchema, type SearchFormValues } from './schema';
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { Plus, Edit, Trash2, FolderKanban, Activity, Database, Layers, Boxes } from 'lucide-react';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
const ProjectGroupList: React.FC = () => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [currentProject, setCurrentProject] = useState<ProjectGroup>();
|
||||
const [list, setList] = useState<ProjectGroup[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pagination, setPagination] = useState({
|
||||
pageNum: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
totalElements: 0,
|
||||
});
|
||||
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm<SearchFormValues>({
|
||||
resolver: zodResolver(searchFormSchema),
|
||||
defaultValues: {
|
||||
projectGroupCode: '',
|
||||
projectGroupName: '',
|
||||
type: undefined,
|
||||
enabled: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const loadData = async (params?: SearchFormValues) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const queryParams: ProjectGroupQueryParams = {
|
||||
...params,
|
||||
pageNum: pagination.pageNum,
|
||||
pageSize: pagination.pageSize,
|
||||
};
|
||||
const data = await getProjectGroupPage(queryParams);
|
||||
setList(data.content || []);
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
totalElements: data.totalElements,
|
||||
}));
|
||||
// 计算统计数据
|
||||
const all = data.content || [];
|
||||
setStats({
|
||||
total: all.length,
|
||||
enabled: all.filter((item) => item.enabled).length,
|
||||
disabled: all.filter((item) => !item.enabled).length,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '加载数据失败,请稍后重试',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
pageNum: page,
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData(form.getValues());
|
||||
}, [pagination.pageNum, pagination.pageSize]);
|
||||
|
||||
const handleEdit = (project: ProjectGroup) => {
|
||||
setCurrentProject(project);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setModalVisible(false);
|
||||
setCurrentProject(undefined);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
setModalVisible(false);
|
||||
setCurrentProject(undefined);
|
||||
loadData(form.getValues());
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-bold tracking-tight">项目组管理</h2>
|
||||
<Button onClick={() => {
|
||||
setCurrentProject(undefined);
|
||||
setModalVisible(true);
|
||||
}}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建项目组
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">总项目组数</CardTitle>
|
||||
<FolderKanban className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.total}</div>
|
||||
<p className="text-xs text-muted-foreground">所有项目组</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">已启用</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.enabled}</div>
|
||||
<p className="text-xs text-muted-foreground">正在使用中</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">已禁用</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-gray-600">{stats.disabled}</div>
|
||||
<p className="text-xs text-muted-foreground">暂时停用</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 搜索过滤 */}
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
placeholder="项目组编码"
|
||||
value={form.watch('projectGroupCode')}
|
||||
onChange={(e) => form.setValue('projectGroupCode', e.target.value)}
|
||||
className="max-w-[200px]"
|
||||
/>
|
||||
<Input
|
||||
placeholder="项目组名称"
|
||||
value={form.watch('projectGroupName')}
|
||||
onChange={(e) => form.setValue('projectGroupName', e.target.value)}
|
||||
className="max-w-[200px]"
|
||||
/>
|
||||
<Select
|
||||
value={form.watch('type')}
|
||||
onValueChange={(value) => form.setValue('type', value as ProjectGroupTypeEnum)}
|
||||
>
|
||||
<SelectTrigger className="max-w-[200px]">
|
||||
<SelectValue placeholder="项目组类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ProjectGroupTypeEnum.PRODUCT}>产品型</SelectItem>
|
||||
<SelectItem value={ProjectGroupTypeEnum.PROJECT}>项目型</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={form.watch('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 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>
|
||||
<TableHead className="w-[140px]">项目组编码</TableHead>
|
||||
<TableHead className="w-[140px]">项目组名称</TableHead>
|
||||
<TableHead className="w-[200px]">项目组描述</TableHead>
|
||||
<TableHead className="w-[120px]">项目组类型</TableHead>
|
||||
<TableHead className="w-[100px]">环境数量</TableHead>
|
||||
<TableHead className="w-[100px]">项目数量</TableHead>
|
||||
<TableHead className="w-[80px]">状态</TableHead>
|
||||
<TableHead className="w-[80px]">排序</TableHead>
|
||||
<TableHead className="w-[180px]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="h-24 text-center">
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
list.map((item) => {
|
||||
const typeInfo = getProjectTypeInfo(item.type);
|
||||
return (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-medium">{item.projectGroupCode}</TableCell>
|
||||
<TableCell>{item.projectGroupName}</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{item.projectGroupDesc}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="inline-flex items-center gap-1">
|
||||
{typeInfo.icon}
|
||||
{typeInfo.label}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Layers className="h-3 w-3 text-muted-foreground" />
|
||||
<span>{item.totalEnvironments}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Boxes className="h-3 w-3 text-muted-foreground" />
|
||||
<span>{item.totalApplications}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={item.enabled ? 'default' : 'secondary'}
|
||||
className="inline-flex"
|
||||
>
|
||||
{item.enabled ? '启用' : '禁用'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{item.sort}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setCurrentProject(item);
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex justify-end border-t border-border bg-muted/40">
|
||||
<DataTablePagination
|
||||
pageIndex={pagination.pageNum}
|
||||
pageSize={pagination.pageSize}
|
||||
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 对话框 */}
|
||||
<ProjectGroupModal
|
||||
open={modalVisible}
|
||||
onCancel={handleModalClose}
|
||||
onSuccess={handleSuccess}
|
||||
initialValues={currentProject}
|
||||
/>
|
||||
<DeleteDialog
|
||||
open={deleteDialogOpen}
|
||||
record={currentProject}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectGroupList;
|
||||
@ -1,46 +0,0 @@
|
||||
import * as z from "zod";
|
||||
import {ProjectGroupTypeEnum} from "./types";
|
||||
|
||||
export const searchFormSchema = z.object({
|
||||
projectGroupCode: z.string().optional(),
|
||||
projectGroupName: z.string().optional(),
|
||||
type: z.nativeEnum(ProjectGroupTypeEnum).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const formSchema = z.object({
|
||||
projectGroupCode: z.string({
|
||||
required_error: "项目组编码不能为空",
|
||||
invalid_type_error: "项目组编码格式不正确",
|
||||
})
|
||||
.min(1, "项目组编码不能为空")
|
||||
.max(50, "项目组编码不能超过50个字符")
|
||||
.regex(/^[a-z0-9-]+$/, "项目组编码只能包含小写字母、数字和连字符"),
|
||||
projectGroupName: z.string({
|
||||
required_error: "项目组名称不能为空",
|
||||
invalid_type_error: "项目组名称格式不正确",
|
||||
})
|
||||
.min(1, "项目组名称不能为空")
|
||||
.max(50, "项目组名称不能超过50个字符"),
|
||||
type: z.nativeEnum(ProjectGroupTypeEnum, {
|
||||
required_error: "请选择项目组类型",
|
||||
invalid_type_error: "项目组类型无效",
|
||||
}),
|
||||
projectGroupDesc: z.string()
|
||||
.max(200, "项目组描述不能超过200个字符")
|
||||
.nullable()
|
||||
.optional()
|
||||
.transform(val => 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<typeof searchFormSchema>;
|
||||
export type FormValues = z.infer<typeof formSchema>;
|
||||
@ -1,41 +0,0 @@
|
||||
import request from '@/utils/request';
|
||||
import type { CreateProjectGroupRequest, UpdateProjectGroupRequest, ProjectGroup, ProjectGroupQueryParams } from './types';
|
||||
import type { Page } from '@/types/base';
|
||||
|
||||
const BASE_URL = '/api/v1/project-group';
|
||||
|
||||
// 创建项目
|
||||
export const createProjectGroup = (data: CreateProjectGroupRequest) =>
|
||||
request.post<void>(BASE_URL, data);
|
||||
|
||||
// 更新项目
|
||||
export const updateProjectGroup = (data: UpdateProjectGroupRequest) =>
|
||||
request.put<void>(`${BASE_URL}/${data.id}`, data);
|
||||
|
||||
// 删除项目
|
||||
export const deleteProjectGroup = (id: number) =>
|
||||
request.delete<void>(`${BASE_URL}/${id}`);
|
||||
|
||||
// 获取项目详情
|
||||
export const getProjectGroup = (id: number) =>
|
||||
request.get<ProjectGroup>(`${BASE_URL}/${id}`);
|
||||
|
||||
// 分页查询项目列表
|
||||
export const getProjectGroupPage = (params?: ProjectGroupQueryParams) =>
|
||||
request.get<Page<ProjectGroup>>(`${BASE_URL}/page`, { params });
|
||||
|
||||
// 获取所有项目列表
|
||||
export const getProjectGroupList = () =>
|
||||
request.get<ProjectGroup[]>(BASE_URL);
|
||||
|
||||
// 条件查询项目列表
|
||||
export const getProjectGroupListByCondition = (params?: ProjectGroupQueryParams) =>
|
||||
request.get<ProjectGroup[]>(`${BASE_URL}/list`, { params });
|
||||
|
||||
// 批量处理项目
|
||||
export const batchProcessProjectGroup = (data: any) =>
|
||||
request.post<void>(`${BASE_URL}/batch`, data);
|
||||
|
||||
// 导出项目数据
|
||||
export const exportProjectGroup = () =>
|
||||
request.get(`${BASE_URL}/export`, { responseType: 'blob' });
|
||||
@ -1,42 +0,0 @@
|
||||
import {BaseResponse, BaseRequest, BaseQuery} from '@/types/base';
|
||||
|
||||
export enum ProjectGroupTypeEnum {
|
||||
PRODUCT = 'PRODUCT',
|
||||
PROJECT = 'PROJECT'
|
||||
}
|
||||
|
||||
// 项目基础信息
|
||||
export interface ProjectGroup extends BaseResponse {
|
||||
tenantCode: string;
|
||||
type: ProjectGroupTypeEnum;
|
||||
projectGroupCode: string;
|
||||
projectGroupName: string;
|
||||
projectGroupDesc?: string;
|
||||
enabled: boolean;
|
||||
totalEnvironments: number;
|
||||
totalApplications: number;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
// 创建项目请求参数
|
||||
export interface CreateProjectGroupRequest extends BaseRequest {
|
||||
tenantCode: string;
|
||||
type: ProjectGroupTypeEnum;
|
||||
projectGroupCode: string;
|
||||
projectGroupName: string;
|
||||
projectGroupDesc?: string;
|
||||
enabled: boolean;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
// 更新项目请求参数
|
||||
export interface UpdateProjectGroupRequest extends CreateProjectGroupRequest {
|
||||
id: number;
|
||||
}
|
||||
|
||||
// 分页查询参数
|
||||
export interface ProjectGroupQueryParams extends BaseQuery {
|
||||
projectGroupName?: string;
|
||||
projectGroupCode?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import {ProjectOutlined, RocketOutlined} from '@ant-design/icons';
|
||||
import {ProjectGroupTypeEnum} from './types';
|
||||
|
||||
interface ProjectTypeInfo {
|
||||
type: ProjectGroupTypeEnum;
|
||||
label: string;
|
||||
color: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
// 获取项目组类型信息
|
||||
export const getProjectTypeInfo = (type: ProjectGroupTypeEnum): ProjectTypeInfo => {
|
||||
switch (type) {
|
||||
case ProjectGroupTypeEnum.PRODUCT:
|
||||
return {
|
||||
type: ProjectGroupTypeEnum.PRODUCT,
|
||||
label: '产品型',
|
||||
color: '#1890ff',
|
||||
icon: <RocketOutlined/>,
|
||||
};
|
||||
case ProjectGroupTypeEnum.PROJECT:
|
||||
return {
|
||||
type: ProjectGroupTypeEnum.PROJECT,
|
||||
label: '项目型',
|
||||
color: '#52c41a',
|
||||
icon: <ProjectOutlined/>,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
type: type,
|
||||
label: type || '未知',
|
||||
color: '#666666',
|
||||
icon: <ProjectOutlined/>,
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -37,7 +37,6 @@ const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
|
||||
const LogStreamPage = lazy(() => import('../pages/LogStream'));
|
||||
const NodeDesign = lazy(() => import('../pages/Workflow/NodeDesign'));
|
||||
const NodeDesignForm = lazy(() => import('../pages/Workflow/NodeDesign/Design'));
|
||||
const ProjectGroupList = lazy(() => import('../pages/Deploy/ProjectGroup/List'));
|
||||
const ApplicationList = lazy(() => import('../pages/Deploy/Application/List'));
|
||||
const EnvironmentList = lazy(() => import('../pages/Deploy/Environment/List'));
|
||||
const DeploymentConfigList = lazy(() => import('../pages/Deploy/Deployment/List'));
|
||||
@ -81,10 +80,6 @@ const router = createBrowserRouter([
|
||||
{
|
||||
path: 'deploy',
|
||||
children: [
|
||||
{
|
||||
path: 'project-group',
|
||||
element: <Suspense fallback={<LoadingComponent/>}><ProjectGroupList/></Suspense>
|
||||
},
|
||||
{
|
||||
path: 'applications',
|
||||
element: <Suspense fallback={<LoadingComponent/>}><ApplicationList/></Suspense>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user