增加团队管理页面

This commit is contained in:
dengqichen 2025-10-29 09:40:37 +08:00
parent 2980e12980
commit 5eb830fe02
14 changed files with 1505 additions and 0 deletions

View File

@ -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: '外部系统',

View File

@ -17,6 +17,7 @@ export interface Application extends BaseResponse {
language: DevelopmentLanguageTypeEnum;
enabled: boolean;
sort: number;
teamCount?: number;
categoryId: number;
category?: ApplicationCategoryResponse;
externalSystemId?: number;

View 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;

View File

@ -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;

View 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;

View 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;

View 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>;

View 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}`);

View 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;
}

View 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>;

View 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}`);

View 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;
}

View File

@ -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`);

View File

@ -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>