增加团队管理页面
This commit is contained in:
parent
2980e12980
commit
5eb830fe02
@ -226,6 +226,16 @@ const ApplicationList: React.FC = () => {
|
|||||||
header: '应用描述',
|
header: '应用描述',
|
||||||
size: 180,
|
size: 180,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'teamCount',
|
||||||
|
header: '团队数量',
|
||||||
|
size: 100,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="font-medium">{row.original.teamCount || 0}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'externalSystem',
|
id: 'externalSystem',
|
||||||
header: '外部系统',
|
header: '外部系统',
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export interface Application extends BaseResponse {
|
|||||||
language: DevelopmentLanguageTypeEnum;
|
language: DevelopmentLanguageTypeEnum;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
sort: number;
|
sort: number;
|
||||||
|
teamCount?: number;
|
||||||
categoryId: number;
|
categoryId: number;
|
||||||
category?: ApplicationCategoryResponse;
|
category?: ApplicationCategoryResponse;
|
||||||
externalSystemId?: number;
|
externalSystemId?: number;
|
||||||
|
|||||||
124
frontend/src/pages/Deploy/Team/List/components/DeleteDialog.tsx
Normal file
124
frontend/src/pages/Deploy/Team/List/components/DeleteDialog.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import { deleteTeam } from '../service';
|
||||||
|
import type { TeamResponse } from '../types';
|
||||||
|
|
||||||
|
interface DeleteDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
record?: TeamResponse;
|
||||||
|
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 deleteTeam(record.id);
|
||||||
|
toast({ title: '删除成功', description: `团队 "${record.teamName}" 已删除` });
|
||||||
|
onSuccess();
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
toast({
|
||||||
|
title: '删除失败',
|
||||||
|
description: error instanceof Error ? error.message : '未知错误',
|
||||||
|
variant: 'destructive'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!record) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||||
|
确认删除
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
此操作将永久删除该团队,该操作无法撤销。
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-3 py-4">
|
||||||
|
<div className="rounded-lg bg-muted p-4 space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">团队编码:</span>
|
||||||
|
<code className="text-sm font-mono bg-background px-2 py-1 rounded">
|
||||||
|
{record.teamCode}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">团队名称:</span>
|
||||||
|
<span className="text-sm font-medium">{record.teamName}</span>
|
||||||
|
</div>
|
||||||
|
{record.ownerName && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">负责人:</span>
|
||||||
|
<span className="text-sm">{record.ownerName}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{record.memberCount !== undefined && record.memberCount > 0 && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">成员数量:</span>
|
||||||
|
<span className="text-sm font-medium text-destructive">{record.memberCount} 人</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{record.applicationCount !== undefined && record.applicationCount > 0 && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">关联应用:</span>
|
||||||
|
<span className="text-sm font-medium text-destructive">{record.applicationCount} 个</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{((record.memberCount !== undefined && record.memberCount > 0) ||
|
||||||
|
(record.applicationCount !== undefined && record.applicationCount > 0)) && (
|
||||||
|
<div className="flex items-start gap-2 p-3 rounded-lg bg-destructive/10 text-destructive">
|
||||||
|
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||||
|
<p className="text-sm">
|
||||||
|
警告:该团队还有{record.memberCount || 0}名成员和{record.applicationCount || 0}个关联应用,删除后将影响这些关联数据。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleConfirm}>
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteDialog;
|
||||||
|
|
||||||
@ -0,0 +1,468 @@
|
|||||||
|
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 {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { DataTablePagination } from '@/components/ui/pagination';
|
||||||
|
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
Search,
|
||||||
|
Loader2,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
getTeamMembers,
|
||||||
|
createTeamMember,
|
||||||
|
updateTeamMember,
|
||||||
|
deleteTeamMember
|
||||||
|
} from '../../Member/service';
|
||||||
|
import { teamMemberFormSchema, type TeamMemberFormValues } from '../../Member/schema';
|
||||||
|
import type {
|
||||||
|
TeamMemberResponse,
|
||||||
|
TeamMemberQuery
|
||||||
|
} from '../../Member/types';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import { getUserList } from '@/pages/System/User/service';
|
||||||
|
import type { UserResponse } from '@/pages/System/User/types';
|
||||||
|
|
||||||
|
interface MemberManageDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
teamId: number;
|
||||||
|
teamName: string;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队成员管理对话框
|
||||||
|
*/
|
||||||
|
const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
||||||
|
open,
|
||||||
|
teamId,
|
||||||
|
teamName,
|
||||||
|
onOpenChange,
|
||||||
|
onSuccess
|
||||||
|
}) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<Page<TeamMemberResponse> | null>(null);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const [editRecord, setEditRecord] = useState<TeamMemberResponse | null>(null);
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [deleteRecord, setDeleteRecord] = useState<TeamMemberResponse | null>(null);
|
||||||
|
const [users, setUsers] = useState<UserResponse[]>([]);
|
||||||
|
|
||||||
|
// 分页状态
|
||||||
|
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
|
||||||
|
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||||
|
|
||||||
|
const form = useForm<TeamMemberFormValues>({
|
||||||
|
resolver: zodResolver(teamMemberFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
teamId: teamId,
|
||||||
|
userId: undefined,
|
||||||
|
userName: '',
|
||||||
|
roleInTeam: '',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载用户列表
|
||||||
|
const loadUsers = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getUserList();
|
||||||
|
setUsers(result || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户列表失败:', error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "加载用户列表失败",
|
||||||
|
description: error instanceof Error ? error.message : '未知错误',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载成员列表
|
||||||
|
const loadMembers = async () => {
|
||||||
|
if (!teamId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const query: TeamMemberQuery = {
|
||||||
|
teamId: teamId,
|
||||||
|
userName: searchText || undefined,
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
};
|
||||||
|
const result = await getTeamMembers(query);
|
||||||
|
setData(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载成员失败:', error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "加载失败",
|
||||||
|
description: error instanceof Error ? error.message : '未知错误',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
loadUsers();
|
||||||
|
loadMembers();
|
||||||
|
}
|
||||||
|
}, [open, pageNum, pageSize]);
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
setPageNum(0);
|
||||||
|
loadMembers();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新建成员
|
||||||
|
const handleCreate = () => {
|
||||||
|
setEditRecord(null);
|
||||||
|
form.reset({
|
||||||
|
teamId: teamId,
|
||||||
|
userId: undefined,
|
||||||
|
userName: '',
|
||||||
|
roleInTeam: '',
|
||||||
|
});
|
||||||
|
setEditMode(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑成员
|
||||||
|
const handleEdit = (record: TeamMemberResponse) => {
|
||||||
|
setEditRecord(record);
|
||||||
|
form.reset({
|
||||||
|
teamId: record.teamId,
|
||||||
|
userId: record.userId,
|
||||||
|
userName: record.userName || '',
|
||||||
|
roleInTeam: record.roleInTeam || '',
|
||||||
|
});
|
||||||
|
setEditMode(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除成员(打开确认对话框)
|
||||||
|
const handleDeleteClick = (record: TeamMemberResponse) => {
|
||||||
|
setDeleteRecord(record);
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!deleteRecord) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteTeamMember(deleteRecord.id);
|
||||||
|
toast({
|
||||||
|
title: "删除成功",
|
||||||
|
description: `成员 "${deleteRecord.userName}" 已移除`,
|
||||||
|
});
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
setDeleteRecord(null);
|
||||||
|
loadMembers();
|
||||||
|
onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "删除失败",
|
||||||
|
description: error instanceof Error ? error.message : '未知错误',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存表单
|
||||||
|
const handleSave = async (values: TeamMemberFormValues) => {
|
||||||
|
try {
|
||||||
|
if (editRecord) {
|
||||||
|
await updateTeamMember(editRecord.id, values);
|
||||||
|
toast({
|
||||||
|
title: "更新成功",
|
||||||
|
description: `成员 "${values.userName}" 已更新`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await createTeamMember(values);
|
||||||
|
toast({
|
||||||
|
title: "添加成功",
|
||||||
|
description: `成员 "${values.userName}" 已添加`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setEditMode(false);
|
||||||
|
loadMembers();
|
||||||
|
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-4xl max-h-[85vh] flex flex-col p-0">
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-4">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Users className="h-5 w-5" />
|
||||||
|
团队成员管理 - {teamName}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{!editMode ? (
|
||||||
|
<div className="flex-1 overflow-hidden flex flex-col px-6 pb-6">
|
||||||
|
<div className="space-y-4 flex-1 overflow-y-auto">
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<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-[100px]">用户ID</TableHead>
|
||||||
|
<TableHead className="w-[150px]">用户名</TableHead>
|
||||||
|
<TableHead className="w-[150px]">团队角色</TableHead>
|
||||||
|
<TableHead className="w-[180px]">加入时间</TableHead>
|
||||||
|
<TableHead className="w-[100px] text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} 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.userId}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{record.userName || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{record.roleInTeam || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{record.joinTime || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<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={5} className="text-center py-12 text-muted-foreground">
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<Users className="h-12 w-12 opacity-20" />
|
||||||
|
<p className="text-sm">暂无成员</p>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{data && data.totalElements > 0 && (
|
||||||
|
<div className="flex justify-end py-4">
|
||||||
|
<DataTablePagination
|
||||||
|
pageIndex={pageNum}
|
||||||
|
pageSize={pageSize}
|
||||||
|
pageCount={Math.ceil((data.totalElements || 0) / pageSize)}
|
||||||
|
onPageChange={(newPage) => setPageNum(newPage)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSave)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="userId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>选择用户</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const userId = Number(value);
|
||||||
|
const selectedUser = users.find(u => u.id === userId);
|
||||||
|
field.onChange(userId);
|
||||||
|
if (selectedUser) {
|
||||||
|
form.setValue('userName', selectedUser.nickname || selectedUser.username);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={field.value?.toString()}
|
||||||
|
disabled={!!editRecord}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="请选择用户" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{users.map((user) => (
|
||||||
|
<SelectItem key={user.id} value={user.id.toString()}>
|
||||||
|
{user.nickname || user.username} ({user.username})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="roleInTeam"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>团队角色</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入团队角色,如:开发、测试、产品经理等"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
{editRecord ? '更新' : '添加'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
确定要将成员 "{deleteRecord?.userName}" 从团队中移除吗?此操作无法撤销。
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => setDeleteDialogOpen(false)}>
|
||||||
|
取消
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleDeleteConfirm} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||||
|
确认删除
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemberManageDialog;
|
||||||
|
|
||||||
282
frontend/src/pages/Deploy/Team/List/components/TeamModal.tsx
Normal file
282
frontend/src/pages/Deploy/Team/List/components/TeamModal.tsx
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import type { TeamResponse } from '../types';
|
||||||
|
import { createTeam, updateTeam } 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 { 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 { teamFormSchema, type TeamFormValues } from "../schema";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
|
interface TeamModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSuccess: () => void;
|
||||||
|
initialValues?: TeamResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TeamModal: React.FC<TeamModalProps> = ({
|
||||||
|
open,
|
||||||
|
onCancel,
|
||||||
|
onSuccess,
|
||||||
|
initialValues,
|
||||||
|
}) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const isEdit = !!initialValues?.id;
|
||||||
|
|
||||||
|
const form = useForm<TeamFormValues>({
|
||||||
|
resolver: zodResolver(teamFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
teamCode: "",
|
||||||
|
teamName: "",
|
||||||
|
description: "",
|
||||||
|
ownerId: undefined,
|
||||||
|
ownerName: "",
|
||||||
|
enabled: true,
|
||||||
|
sort: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && initialValues) {
|
||||||
|
form.reset({
|
||||||
|
teamCode: initialValues.teamCode,
|
||||||
|
teamName: initialValues.teamName,
|
||||||
|
description: initialValues.description || "",
|
||||||
|
ownerId: initialValues.ownerId,
|
||||||
|
ownerName: initialValues.ownerName || "",
|
||||||
|
enabled: initialValues.enabled,
|
||||||
|
sort: initialValues.sort,
|
||||||
|
});
|
||||||
|
} else if (open && !initialValues) {
|
||||||
|
form.reset({
|
||||||
|
teamCode: "",
|
||||||
|
teamName: "",
|
||||||
|
description: "",
|
||||||
|
ownerId: undefined,
|
||||||
|
ownerName: "",
|
||||||
|
enabled: true,
|
||||||
|
sort: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [open, initialValues, form]);
|
||||||
|
|
||||||
|
const handleSubmit = async (values: TeamFormValues) => {
|
||||||
|
try {
|
||||||
|
if (isEdit) {
|
||||||
|
await updateTeam(initialValues!.id, values as any);
|
||||||
|
toast({
|
||||||
|
title: "更新成功",
|
||||||
|
description: `团队 "${values.teamName}" 已更新`,
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await createTeam(values as any);
|
||||||
|
toast({
|
||||||
|
title: "创建成功",
|
||||||
|
description: `团队 "${values.teamName}" 已创建`,
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onSuccess();
|
||||||
|
onCancel();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: isEdit ? "更新失败" : "创建失败",
|
||||||
|
description: error instanceof Error ? error.message : "未知错误",
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(open) => !open && onCancel()}>
|
||||||
|
<DialogContent className="max-w-[600px] max-h-[85vh] flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{isEdit ? '编辑' : '新建'}团队</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="flex flex-col flex-1 overflow-hidden">
|
||||||
|
<ScrollArea className="flex-1 px-6">
|
||||||
|
<div className="space-y-4 pr-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="teamCode"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>团队编码</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
disabled={isEdit}
|
||||||
|
placeholder="请输入团队编码"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="teamName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>团队名称</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="请输入团队名称" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="ownerId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>负责人ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="请输入负责人ID"
|
||||||
|
{...field}
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
field.onChange(value ? Number(value) : undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="ownerName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>负责人姓名</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="请输入负责人姓名" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>团队描述</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
placeholder="请输入团队描述"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<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>状态</FormLabel>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{field.value ? '启用' : '禁用'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-6" />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
<DialogFooter className="px-6 py-4 border-t mt-0">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
const isValid = await form.trigger();
|
||||||
|
if (isValid) {
|
||||||
|
handleSubmit(form.getValues());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamModal;
|
||||||
|
|
||||||
410
frontend/src/pages/Deploy/Team/List/index.tsx
Normal file
410
frontend/src/pages/Deploy/Team/List/index.tsx
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { PageContainer } from '@/components/ui/page-container';
|
||||||
|
import { getTeams } from './service';
|
||||||
|
import type { TeamResponse, TeamQuery } from './types';
|
||||||
|
import TeamModal from './components/TeamModal';
|
||||||
|
import DeleteDialog from './components/DeleteDialog';
|
||||||
|
import MemberManageDialog from './components/MemberManageDialog';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
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 {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
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 { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
Users,
|
||||||
|
Activity,
|
||||||
|
Server,
|
||||||
|
Database,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
type Column = {
|
||||||
|
accessorKey?: keyof TeamResponse;
|
||||||
|
id?: string;
|
||||||
|
header: string;
|
||||||
|
size?: number;
|
||||||
|
cell?: (props: { row: { original: TeamResponse } }) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TeamList: React.FC = () => {
|
||||||
|
const [list, setList] = useState<TeamResponse[]>([]);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
pageNum: DEFAULT_CURRENT,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
totalElements: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [currentTeam, setCurrentTeam] = useState<TeamResponse | undefined>();
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [memberDialogOpen, setMemberDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<SearchFormValues>({
|
||||||
|
resolver: zodResolver(searchFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
teamCode: '',
|
||||||
|
teamName: '',
|
||||||
|
enabled: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const total = pagination.totalElements;
|
||||||
|
const enabled = list.filter((item) => item.enabled).length;
|
||||||
|
const disabled = list.filter((item) => !item.enabled).length;
|
||||||
|
return { total, enabled, disabled };
|
||||||
|
}, [list, pagination.totalElements]);
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async (searchValues?: SearchFormValues) => {
|
||||||
|
try {
|
||||||
|
const query: TeamQuery = {
|
||||||
|
pageNum: pagination.pageNum - 1,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
teamCode: searchValues?.teamCode || form.getValues('teamCode') || undefined,
|
||||||
|
teamName: searchValues?.teamName || form.getValues('teamName') || undefined,
|
||||||
|
enabled: searchValues?.enabled !== undefined ? searchValues.enabled : form.getValues('enabled'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response: Page<TeamResponse> | undefined = await getTeams(query);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
setList(response.content || []);
|
||||||
|
setPagination({
|
||||||
|
...pagination,
|
||||||
|
totalElements: response.totalElements || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [pagination.pageNum, pagination.pageSize]);
|
||||||
|
|
||||||
|
const handleSuccess = () => {
|
||||||
|
loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setCurrentTeam(undefined);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (record: TeamResponse) => {
|
||||||
|
setCurrentTeam(record);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (record: TeamResponse) => {
|
||||||
|
setCurrentTeam(record);
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleManageMembers = (record: TeamResponse) => {
|
||||||
|
setCurrentTeam(record);
|
||||||
|
setMemberDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalClose = () => {
|
||||||
|
setModalVisible(false);
|
||||||
|
setCurrentTeam(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
setPagination({ ...pagination, pageNum: newPage + 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: Column[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'teamCode',
|
||||||
|
header: '团队编码',
|
||||||
|
size: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'teamName',
|
||||||
|
header: '团队名称',
|
||||||
|
size: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'description',
|
||||||
|
header: '团队描述',
|
||||||
|
size: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'ownerName',
|
||||||
|
header: '负责人',
|
||||||
|
size: 120,
|
||||||
|
cell: ({ row }) => row.original.ownerName || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'memberCount',
|
||||||
|
header: '成员数量',
|
||||||
|
size: 100,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="font-medium">{row.original.memberCount || 0}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'applicationCount',
|
||||||
|
header: '应用数量',
|
||||||
|
size: 100,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="font-medium">{row.original.applicationCount || 0}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'enabled',
|
||||||
|
header: '状态',
|
||||||
|
size: 100,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge variant={row.original.enabled ? 'default' : 'secondary'} className="inline-flex">
|
||||||
|
{row.original.enabled ? '启用' : '禁用'}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'sort',
|
||||||
|
header: '排序',
|
||||||
|
size: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: '操作',
|
||||||
|
size: 200,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => handleManageMembers(row.original)}>
|
||||||
|
<Users className="h-4 w-4 mr-1" />
|
||||||
|
成员
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => handleEdit(row.original)}>
|
||||||
|
<Edit className="h-4 w-4 mr-1" />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(row.original)}
|
||||||
|
className="text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<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>
|
||||||
|
<Server 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>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>团队管理</CardTitle>
|
||||||
|
<CardDescription className="mt-2">
|
||||||
|
创建和管理团队,分配团队成员和应用
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<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('teamCode')}
|
||||||
|
onChange={(e) => form.setValue('teamCode', e.target.value)}
|
||||||
|
className="max-w-[200px]"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="团队名称"
|
||||||
|
value={form.watch('teamName')}
|
||||||
|
onChange={(e) => form.setValue('teamName', e.target.value)}
|
||||||
|
className="max-w-[200px]"
|
||||||
|
/>
|
||||||
|
<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 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.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
暂无数据
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
list.map((item) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
{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>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<div className="flex justify-end border-t border-border bg-muted/40">
|
||||||
|
<DataTablePagination
|
||||||
|
pageIndex={pagination.pageNum - 1}
|
||||||
|
pageSize={pagination.pageSize}
|
||||||
|
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 对话框 */}
|
||||||
|
{modalVisible && (
|
||||||
|
<TeamModal
|
||||||
|
open={modalVisible}
|
||||||
|
onCancel={handleModalClose}
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
initialValues={currentTeam}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<DeleteDialog
|
||||||
|
open={deleteDialogOpen}
|
||||||
|
record={currentTeam}
|
||||||
|
onOpenChange={setDeleteDialogOpen}
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
/>
|
||||||
|
{currentTeam && (
|
||||||
|
<MemberManageDialog
|
||||||
|
open={memberDialogOpen}
|
||||||
|
teamId={currentTeam.id}
|
||||||
|
teamName={currentTeam.teamName}
|
||||||
|
onOpenChange={setMemberDialogOpen}
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamList;
|
||||||
|
|
||||||
27
frontend/src/pages/Deploy/Team/List/schema.ts
Normal file
27
frontend/src/pages/Deploy/Team/List/schema.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索表单校验
|
||||||
|
*/
|
||||||
|
export const searchFormSchema = z.object({
|
||||||
|
teamCode: z.string().optional(),
|
||||||
|
teamName: z.string().optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队表单校验
|
||||||
|
*/
|
||||||
|
export const teamFormSchema = z.object({
|
||||||
|
teamCode: z.string().min(1, '团队编码不能为空'),
|
||||||
|
teamName: z.string().min(1, '团队名称不能为空'),
|
||||||
|
description: z.string().max(500, "团队描述不能超过500个字符").optional(),
|
||||||
|
ownerId: z.number().optional(),
|
||||||
|
ownerName: z.string().optional(),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
sort: z.number().min(0).default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SearchFormValues = z.infer<typeof searchFormSchema>;
|
||||||
|
export type TeamFormValues = z.infer<typeof teamFormSchema>;
|
||||||
|
|
||||||
46
frontend/src/pages/Deploy/Team/List/service.ts
Normal file
46
frontend/src/pages/Deploy/Team/List/service.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import type {
|
||||||
|
TeamQuery,
|
||||||
|
TeamResponse,
|
||||||
|
TeamRequest,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/teams';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询团队
|
||||||
|
*/
|
||||||
|
export const getTeams = (params?: TeamQuery) =>
|
||||||
|
request.get<Page<TeamResponse>>(`${BASE_URL}/page`, { params });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询团队详情
|
||||||
|
*/
|
||||||
|
export const getTeamById = (id: number) =>
|
||||||
|
request.get<TeamResponse>(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有启用的团队(常用)
|
||||||
|
*/
|
||||||
|
export const getEnabledTeams = () =>
|
||||||
|
request.get<TeamResponse[]>(`${BASE_URL}/list`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建团队
|
||||||
|
*/
|
||||||
|
export const createTeam = (data: TeamRequest) =>
|
||||||
|
request.post<TeamResponse>(BASE_URL, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新团队
|
||||||
|
*/
|
||||||
|
export const updateTeam = (id: number, data: TeamRequest) =>
|
||||||
|
request.put<TeamResponse>(`${BASE_URL}/${id}`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除团队
|
||||||
|
*/
|
||||||
|
export const deleteTeam = (id: number) =>
|
||||||
|
request.delete(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
39
frontend/src/pages/Deploy/Team/List/types.ts
Normal file
39
frontend/src/pages/Deploy/Team/List/types.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import type { BaseQuery, BaseResponse } from '@/types/base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队查询参数
|
||||||
|
*/
|
||||||
|
export interface TeamQuery extends BaseQuery {
|
||||||
|
teamCode?: string;
|
||||||
|
teamName?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队响应
|
||||||
|
*/
|
||||||
|
export interface TeamResponse extends BaseResponse {
|
||||||
|
teamCode: string;
|
||||||
|
teamName: string;
|
||||||
|
description?: string;
|
||||||
|
ownerId?: number;
|
||||||
|
ownerName?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
sort: number;
|
||||||
|
memberCount?: number;
|
||||||
|
applicationCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队创建/更新请求
|
||||||
|
*/
|
||||||
|
export interface TeamRequest {
|
||||||
|
teamCode: string;
|
||||||
|
teamName: string;
|
||||||
|
description?: string;
|
||||||
|
ownerId?: number;
|
||||||
|
ownerName?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
sort?: number;
|
||||||
|
}
|
||||||
|
|
||||||
17
frontend/src/pages/Deploy/Team/Member/schema.ts
Normal file
17
frontend/src/pages/Deploy/Team/Member/schema.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队成员表单校验
|
||||||
|
*/
|
||||||
|
export const teamMemberFormSchema = z.object({
|
||||||
|
teamId: z.number(),
|
||||||
|
userId: z.number({
|
||||||
|
required_error: '请输入用户ID',
|
||||||
|
invalid_type_error: '用户ID必须是数字',
|
||||||
|
}),
|
||||||
|
userName: z.string().optional(),
|
||||||
|
roleInTeam: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TeamMemberFormValues = z.infer<typeof teamMemberFormSchema>;
|
||||||
|
|
||||||
40
frontend/src/pages/Deploy/Team/Member/service.ts
Normal file
40
frontend/src/pages/Deploy/Team/Member/service.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import type {
|
||||||
|
TeamMemberQuery,
|
||||||
|
TeamMemberResponse,
|
||||||
|
TeamMemberRequest,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/team-members';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询团队成员
|
||||||
|
*/
|
||||||
|
export const getTeamMembers = (params?: TeamMemberQuery) =>
|
||||||
|
request.get<Page<TeamMemberResponse>>(`${BASE_URL}/page`, { params });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询团队成员详情
|
||||||
|
*/
|
||||||
|
export const getTeamMemberById = (id: number) =>
|
||||||
|
request.get<TeamMemberResponse>(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建团队成员
|
||||||
|
*/
|
||||||
|
export const createTeamMember = (data: TeamMemberRequest) =>
|
||||||
|
request.post<TeamMemberResponse>(BASE_URL, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新团队成员
|
||||||
|
*/
|
||||||
|
export const updateTeamMember = (id: number, data: TeamMemberRequest) =>
|
||||||
|
request.put<TeamMemberResponse>(`${BASE_URL}/${id}`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除团队成员
|
||||||
|
*/
|
||||||
|
export const deleteTeamMember = (id: number) =>
|
||||||
|
request.delete(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
32
frontend/src/pages/Deploy/Team/Member/types.ts
Normal file
32
frontend/src/pages/Deploy/Team/Member/types.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { BaseQuery, BaseResponse } from '@/types/base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队成员查询参数
|
||||||
|
*/
|
||||||
|
export interface TeamMemberQuery extends BaseQuery {
|
||||||
|
teamId?: number;
|
||||||
|
userName?: string;
|
||||||
|
roleInTeam?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队成员响应
|
||||||
|
*/
|
||||||
|
export interface TeamMemberResponse extends BaseResponse {
|
||||||
|
teamId: number;
|
||||||
|
userId: number;
|
||||||
|
userName?: string;
|
||||||
|
roleInTeam?: string;
|
||||||
|
joinTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队成员创建/更新请求
|
||||||
|
*/
|
||||||
|
export interface TeamMemberRequest {
|
||||||
|
teamId: number;
|
||||||
|
userId: number;
|
||||||
|
userName?: string;
|
||||||
|
roleInTeam?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@ -32,3 +32,7 @@ export const assignRoles = (userId: number, roleIds: number[]) =>
|
|||||||
// 获取所有角色列表(不分页)
|
// 获取所有角色列表(不分页)
|
||||||
export const getAllRoles = () =>
|
export const getAllRoles = () =>
|
||||||
request.get<Role[]>(`${ROLE_BASE_URL}/list`);
|
request.get<Role[]>(`${ROLE_BASE_URL}/list`);
|
||||||
|
|
||||||
|
// 获取所有用户列表(不分页)
|
||||||
|
export const getUserList = () =>
|
||||||
|
request.get<UserResponse[]>(`${BASE_URL}/list`);
|
||||||
@ -43,6 +43,7 @@ const DeploymentConfigList = lazy(() => import('../pages/Deploy/Deployment/List'
|
|||||||
const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/List'));
|
const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/List'));
|
||||||
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
||||||
const External = lazy(() => import('../pages/Deploy/External'));
|
const External = lazy(() => import('../pages/Deploy/External'));
|
||||||
|
const TeamList = lazy(() => import('../pages/Deploy/Team/List'));
|
||||||
const FormDesigner = lazy(() => import('../pages/FormDesigner'));
|
const FormDesigner = lazy(() => import('../pages/FormDesigner'));
|
||||||
const FormDefinitionList = lazy(() => import('../pages/Form/Definition'));
|
const FormDefinitionList = lazy(() => import('../pages/Form/Definition'));
|
||||||
const FormDefinitionDesigner = lazy(() => import('../pages/Form/Definition/Designer'));
|
const FormDefinitionDesigner = lazy(() => import('../pages/Form/Definition/Designer'));
|
||||||
@ -84,6 +85,10 @@ const router = createBrowserRouter([
|
|||||||
path: 'applications',
|
path: 'applications',
|
||||||
element: <Suspense fallback={<LoadingComponent/>}><ApplicationList/></Suspense>
|
element: <Suspense fallback={<LoadingComponent/>}><ApplicationList/></Suspense>
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'teams',
|
||||||
|
element: <Suspense fallback={<LoadingComponent/>}><TeamList/></Suspense>
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'deployment',
|
path: 'deployment',
|
||||||
element: <Suspense fallback={<LoadingComponent/>}><DeploymentConfigList/></Suspense>
|
element: <Suspense fallback={<LoadingComponent/>}><DeploymentConfigList/></Suspense>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user