+
+ {/* 搜索栏 */}
+
+
+
+ setSearchText(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
+ className="pl-10"
+ />
+
+
+
+
+ {/* 表格 */}
+
+
+
+
+ 用户ID
+ 用户名
+ 团队角色
+ 加入时间
+ 操作
+
+
+
+ {loading ? (
+
+
+
+
+ 加载中...
+
+
+
+ ) : data?.content && data.content.length > 0 ? (
+ data.content.map((record) => (
+
+
+ {record.userId}
+
+
+ {record.userName || '-'}
+
+
+ {record.roleInTeam || '-'}
+
+
+ {record.joinTime || '-'}
+
+
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+ {/* 分页 */}
+ {data && data.totalElements > 0 && (
+
+ setPageNum(newPage)}
+ />
+
+ )}
+
+
+ ) : (
+
+
+ )}
+
+
+
+ {/* 删除确认对话框 */}
+
+
+
+ 确认删除
+
+ 确定要将成员 "{deleteRecord?.userName}" 从团队中移除吗?此操作无法撤销。
+
+
+
+ setDeleteDialogOpen(false)}>
+ 取消
+
+
+ 确认删除
+
+
+
+
+ >
+ );
+};
+
+export default MemberManageDialog;
+
diff --git a/frontend/src/pages/Deploy/Team/List/components/TeamModal.tsx b/frontend/src/pages/Deploy/Team/List/components/TeamModal.tsx
new file mode 100644
index 00000000..f352ad59
--- /dev/null
+++ b/frontend/src/pages/Deploy/Team/List/components/TeamModal.tsx
@@ -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
= ({
+ open,
+ onCancel,
+ onSuccess,
+ initialValues,
+}) => {
+ const { toast } = useToast();
+ const isEdit = !!initialValues?.id;
+
+ const form = useForm({
+ 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 (
+
+ );
+};
+
+export default TeamModal;
+
diff --git a/frontend/src/pages/Deploy/Team/List/index.tsx b/frontend/src/pages/Deploy/Team/List/index.tsx
new file mode 100644
index 00000000..0c2f3d1f
--- /dev/null
+++ b/frontend/src/pages/Deploy/Team/List/index.tsx
@@ -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([]);
+ const [pagination, setPagination] = useState({
+ pageNum: DEFAULT_CURRENT,
+ pageSize: DEFAULT_PAGE_SIZE,
+ totalElements: 0,
+ });
+
+ const [modalVisible, setModalVisible] = useState(false);
+ const [currentTeam, setCurrentTeam] = useState();
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [memberDialogOpen, setMemberDialogOpen] = useState(false);
+
+ const form = useForm({
+ 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 | 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 }) => (
+
+ {row.original.memberCount || 0}
+
+ ),
+ },
+ {
+ id: 'applicationCount',
+ header: '应用数量',
+ size: 100,
+ cell: ({ row }) => (
+
+ {row.original.applicationCount || 0}
+
+ ),
+ },
+ {
+ accessorKey: 'enabled',
+ header: '状态',
+ size: 100,
+ cell: ({ row }) => (
+
+ {row.original.enabled ? '启用' : '禁用'}
+
+ ),
+ },
+ {
+ accessorKey: 'sort',
+ header: '排序',
+ size: 80,
+ },
+ {
+ id: 'actions',
+ header: '操作',
+ size: 200,
+ cell: ({ row }) => (
+
+
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+ {/* 统计卡片 */}
+
+
+
+ 总团队数
+
+
+
+ {stats.total}
+ 所有团队
+
+
+
+
+ 已启用
+
+
+
+ {stats.enabled}
+ 正在使用中
+
+
+
+
+ 已禁用
+
+
+
+ {stats.disabled}
+ 暂时停用
+
+
+
+
+ {/* 团队管理 */}
+
+
+
+
+ 团队管理
+
+ 创建和管理团队,分配团队成员和应用
+
+
+
+
+
+
+
+ {/* 搜索过滤 */}
+
+ form.setValue('teamCode', e.target.value)}
+ className="max-w-[200px]"
+ />
+ form.setValue('teamName', e.target.value)}
+ className="max-w-[200px]"
+ />
+
+
+
+
+
+ {/* 数据表格 */}
+
+
+
+
+ {columns.map((column) => (
+
+ {column.header}
+
+ ))}
+
+
+
+ {list.length === 0 ? (
+
+
+ 暂无数据
+
+
+ ) : (
+ list.map((item) => (
+
+ {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 (
+
+ {cellContent}
+
+ );
+ })}
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+ {/* 对话框 */}
+ {modalVisible && (
+
+ )}
+
+ {currentTeam && (
+
+ )}
+
+ );
+};
+
+export default TeamList;
+
diff --git a/frontend/src/pages/Deploy/Team/List/schema.ts b/frontend/src/pages/Deploy/Team/List/schema.ts
new file mode 100644
index 00000000..2e882b0d
--- /dev/null
+++ b/frontend/src/pages/Deploy/Team/List/schema.ts
@@ -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;
+export type TeamFormValues = z.infer;
+
diff --git a/frontend/src/pages/Deploy/Team/List/service.ts b/frontend/src/pages/Deploy/Team/List/service.ts
new file mode 100644
index 00000000..aa83af8b
--- /dev/null
+++ b/frontend/src/pages/Deploy/Team/List/service.ts
@@ -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>(`${BASE_URL}/page`, { params });
+
+/**
+ * 查询团队详情
+ */
+export const getTeamById = (id: number) =>
+ request.get(`${BASE_URL}/${id}`);
+
+/**
+ * 查询所有启用的团队(常用)
+ */
+export const getEnabledTeams = () =>
+ request.get(`${BASE_URL}/list`);
+
+/**
+ * 创建团队
+ */
+export const createTeam = (data: TeamRequest) =>
+ request.post(BASE_URL, data);
+
+/**
+ * 更新团队
+ */
+export const updateTeam = (id: number, data: TeamRequest) =>
+ request.put(`${BASE_URL}/${id}`, data);
+
+/**
+ * 删除团队
+ */
+export const deleteTeam = (id: number) =>
+ request.delete(`${BASE_URL}/${id}`);
+
diff --git a/frontend/src/pages/Deploy/Team/List/types.ts b/frontend/src/pages/Deploy/Team/List/types.ts
new file mode 100644
index 00000000..9b1276ab
--- /dev/null
+++ b/frontend/src/pages/Deploy/Team/List/types.ts
@@ -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;
+}
+
diff --git a/frontend/src/pages/Deploy/Team/Member/schema.ts b/frontend/src/pages/Deploy/Team/Member/schema.ts
new file mode 100644
index 00000000..af4e2299
--- /dev/null
+++ b/frontend/src/pages/Deploy/Team/Member/schema.ts
@@ -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;
+
diff --git a/frontend/src/pages/Deploy/Team/Member/service.ts b/frontend/src/pages/Deploy/Team/Member/service.ts
new file mode 100644
index 00000000..f12444fb
--- /dev/null
+++ b/frontend/src/pages/Deploy/Team/Member/service.ts
@@ -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>(`${BASE_URL}/page`, { params });
+
+/**
+ * 查询团队成员详情
+ */
+export const getTeamMemberById = (id: number) =>
+ request.get(`${BASE_URL}/${id}`);
+
+/**
+ * 创建团队成员
+ */
+export const createTeamMember = (data: TeamMemberRequest) =>
+ request.post(BASE_URL, data);
+
+/**
+ * 更新团队成员
+ */
+export const updateTeamMember = (id: number, data: TeamMemberRequest) =>
+ request.put(`${BASE_URL}/${id}`, data);
+
+/**
+ * 删除团队成员
+ */
+export const deleteTeamMember = (id: number) =>
+ request.delete(`${BASE_URL}/${id}`);
+
diff --git a/frontend/src/pages/Deploy/Team/Member/types.ts b/frontend/src/pages/Deploy/Team/Member/types.ts
new file mode 100644
index 00000000..4284e6c0
--- /dev/null
+++ b/frontend/src/pages/Deploy/Team/Member/types.ts
@@ -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;
+}
+
diff --git a/frontend/src/pages/System/User/service.ts b/frontend/src/pages/System/User/service.ts
index 49cd7287..c41ab578 100644
--- a/frontend/src/pages/System/User/service.ts
+++ b/frontend/src/pages/System/User/service.ts
@@ -32,3 +32,7 @@ export const assignRoles = (userId: number, roleIds: number[]) =>
// 获取所有角色列表(不分页)
export const getAllRoles = () =>
request.get(`${ROLE_BASE_URL}/list`);
+
+// 获取所有用户列表(不分页)
+export const getUserList = () =>
+ request.get(`${BASE_URL}/list`);
\ No newline at end of file
diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx
index d18c4858..1d2a5a42 100644
--- a/frontend/src/router/index.tsx
+++ b/frontend/src/router/index.tsx
@@ -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: }>
},
+ {
+ path: 'teams',
+ element: }>
+ },
{
path: 'deployment',
element: }>