增加团队管理页面
This commit is contained in:
parent
2980e12980
commit
5eb830fe02
@ -226,6 +226,16 @@ const ApplicationList: React.FC = () => {
|
||||
header: '应用描述',
|
||||
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',
|
||||
header: '外部系统',
|
||||
|
||||
@ -17,6 +17,7 @@ export interface Application extends BaseResponse {
|
||||
language: DevelopmentLanguageTypeEnum;
|
||||
enabled: boolean;
|
||||
sort: number;
|
||||
teamCount?: number;
|
||||
categoryId: number;
|
||||
category?: ApplicationCategoryResponse;
|
||||
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 = () =>
|
||||
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 GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
||||
const External = lazy(() => import('../pages/Deploy/External'));
|
||||
const TeamList = lazy(() => import('../pages/Deploy/Team/List'));
|
||||
const FormDesigner = lazy(() => import('../pages/FormDesigner'));
|
||||
const FormDefinitionList = lazy(() => import('../pages/Form/Definition'));
|
||||
const FormDefinitionDesigner = lazy(() => import('../pages/Form/Definition/Designer'));
|
||||
@ -84,6 +85,10 @@ const router = createBrowserRouter([
|
||||
path: 'applications',
|
||||
element: <Suspense fallback={<LoadingComponent/>}><ApplicationList/></Suspense>
|
||||
},
|
||||
{
|
||||
path: 'teams',
|
||||
element: <Suspense fallback={<LoadingComponent/>}><TeamList/></Suspense>
|
||||
},
|
||||
{
|
||||
path: 'deployment',
|
||||
element: <Suspense fallback={<LoadingComponent/>}><DeploymentConfigList/></Suspense>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user