From 0a5c60b888b6bf938000677d2bb2ef54c776b2b6 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Wed, 29 Oct 2025 10:32:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=9B=A2=E9=98=9F=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../List/components/ApplicationModal.tsx | 6 +- .../pages/Deploy/Application/List/index.tsx | 2 +- .../pages/Deploy/Application/List/schema.ts | 2 +- .../pages/Deploy/Application/List/types.ts | 4 +- .../components/ApplicationManageDialog.tsx | 427 ++++++++++++++++++ .../List/components/MemberManageDialog.tsx | 319 ++++++------- 6 files changed, 580 insertions(+), 180 deletions(-) create mode 100644 frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx diff --git a/frontend/src/pages/Deploy/Application/List/components/ApplicationModal.tsx b/frontend/src/pages/Deploy/Application/List/components/ApplicationModal.tsx index b13962a0..27104a66 100644 --- a/frontend/src/pages/Deploy/Application/List/components/ApplicationModal.tsx +++ b/frontend/src/pages/Deploy/Application/List/components/ApplicationModal.tsx @@ -74,7 +74,7 @@ const ApplicationModal: React.FC = ({ appCode: "", appName: "", appDesc: "", - categoryId: undefined, + applicationCategoryId: undefined, language: DevelopmentLanguageTypeEnum.JAVA, enabled: true, sort: 0, @@ -131,7 +131,7 @@ const ApplicationModal: React.FC = ({ appCode: initialValues.appCode, appName: initialValues.appName, appDesc: initialValues.appDesc || "", - categoryId: initialValues.categoryId, + applicationCategoryId: initialValues.applicationCategoryId, language: initialValues.language, enabled: initialValues.enabled, sort: initialValues.sort, @@ -221,7 +221,7 @@ const ApplicationModal: React.FC = ({
( 应用分类 diff --git a/frontend/src/pages/Deploy/Application/List/index.tsx b/frontend/src/pages/Deploy/Application/List/index.tsx index d87d9e2b..363fa37e 100644 --- a/frontend/src/pages/Deploy/Application/List/index.tsx +++ b/frontend/src/pages/Deploy/Application/List/index.tsx @@ -104,7 +104,7 @@ const ApplicationList: React.FC = () => { try { const queryParams: ApplicationQuery = { ...params, - categoryId: selectedCategoryId, + applicationCategoryId: selectedCategoryId, pageNum: pagination.pageNum, pageSize: pagination.pageSize, }; diff --git a/frontend/src/pages/Deploy/Application/List/schema.ts b/frontend/src/pages/Deploy/Application/List/schema.ts index 5218dc58..7a7a5f7f 100644 --- a/frontend/src/pages/Deploy/Application/List/schema.ts +++ b/frontend/src/pages/Deploy/Application/List/schema.ts @@ -12,7 +12,7 @@ export const applicationFormSchema = z.object({ appCode: z.string().min(1, '应用编码不能为空'), appName: z.string().min(1, '应用名称不能为空'), appDesc: z.string().max(200, "应用描述不能超过200个字符").optional(), - categoryId: z.number({ + applicationCategoryId: z.number({ required_error: '请选择应用分类', invalid_type_error: '应用分类必须是数字', }), diff --git a/frontend/src/pages/Deploy/Application/List/types.ts b/frontend/src/pages/Deploy/Application/List/types.ts index ceb8af07..f30dfecc 100644 --- a/frontend/src/pages/Deploy/Application/List/types.ts +++ b/frontend/src/pages/Deploy/Application/List/types.ts @@ -18,7 +18,7 @@ export interface Application extends BaseResponse { enabled: boolean; sort: number; teamCount?: number; - categoryId: number; + applicationCategoryId: number; category?: ApplicationCategoryResponse; externalSystemId?: number; externalSystem?: ExternalSystemResponse; @@ -66,7 +66,7 @@ export type ApplicationFormValues = CreateApplicationRequest; export interface ApplicationQuery extends BaseQuery { appCode?: string; appName?: string; - categoryId?: number; + applicationCategoryId?: number; enabled?: boolean; language?: string; } diff --git a/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx new file mode 100644 index 00000000..2dd28861 --- /dev/null +++ b/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx @@ -0,0 +1,427 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; +import { useToast } from "@/components/ui/use-toast"; +import { DataTablePagination } from '@/components/ui/pagination'; +import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; +import { + Plus, + Trash2, + Search, + Loader2, + Package, + Check, + ChevronsUpDown, +} from "lucide-react"; +import { + getTeamApplications, + createTeamApplication, + deleteTeamApplication +} from '../../Application/service'; +import type { + TeamApplicationResponse, + TeamApplicationQuery +} from '../../Application/types'; +import type { Page } from '@/types/base'; +import type { Application } from '@/pages/Deploy/Application/List/types'; + +interface ApplicationManageDialogProps { + open: boolean; + teamId: number; + teamName: string; + applications: Application[]; + onOpenChange: (open: boolean) => void; + onSuccess?: () => void; +} + +/** + * 团队应用管理对话框 + */ +const ApplicationManageDialog: React.FC = ({ + open, + teamId, + teamName, + applications, + onOpenChange, + onSuccess +}) => { + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState | null>(null); + const [searchText, setSearchText] = useState(''); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteRecord, setDeleteRecord] = useState(null); + const [selectedAppIds, setSelectedAppIds] = useState([]); + const [popoverOpen, setPopoverOpen] = useState(false); + const [adding, setAdding] = useState(false); + + // 分页状态 + const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1); + const [pageSize] = useState(DEFAULT_PAGE_SIZE); + + + // 加载应用列表 + const loadApplications = async () => { + if (!teamId) return; + + setLoading(true); + try { + const query: TeamApplicationQuery = { + teamId: teamId, + pageNum, + pageSize, + }; + const result = await getTeamApplications(query); + setData(result); + } catch (error) { + console.error('加载应用失败:', error); + toast({ + variant: "destructive", + title: "加载失败", + description: error instanceof Error ? error.message : '未知错误', + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (open) { + loadApplications(); + } + }, [open, pageNum, pageSize]); + + // 搜索 + const handleSearch = () => { + setPageNum(0); + loadApplications(); + }; + + // 切换应用选择 + const toggleAppSelection = (appId: number) => { + setSelectedAppIds(prev => + prev.includes(appId) + ? prev.filter(id => id !== appId) + : [...prev, appId] + ); + }; + + // 批量添加应用 + const handleBatchAdd = async () => { + if (selectedAppIds.length === 0) { + toast({ + variant: "destructive", + title: "请选择应用", + description: "至少选择一个应用进行添加", + }); + return; + } + + setAdding(true); + try { + // 并行创建所有关联 + await Promise.all( + selectedAppIds.map(appId => + createTeamApplication({ + teamId, + applicationId: appId, + }) + ) + ); + + toast({ + title: "添加成功", + description: `已成功添加 ${selectedAppIds.length} 个应用`, + }); + + setSelectedAppIds([]); + setPopoverOpen(false); + loadApplications(); + onSuccess?.(); + } catch (error) { + console.error('批量添加失败:', error); + toast({ + variant: "destructive", + title: "添加失败", + description: error instanceof Error ? error.message : '未知错误', + }); + } finally { + setAdding(false); + } + }; + + // 删除应用关联(打开确认对话框) + const handleDeleteClick = (record: TeamApplicationResponse) => { + setDeleteRecord(record); + setDeleteDialogOpen(true); + }; + + // 确认删除 + const handleDeleteConfirm = async () => { + if (!deleteRecord) return; + + try { + await deleteTeamApplication(deleteRecord.id); + toast({ + title: "删除成功", + description: `应用 "${deleteRecord.applicationName}" 已移除`, + }); + setDeleteDialogOpen(false); + setDeleteRecord(null); + loadApplications(); + onSuccess?.(); + } catch (error) { + console.error('删除失败:', error); + toast({ + variant: "destructive", + title: "删除失败", + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 过滤出未关联的应用 + const availableApplications = applications.filter(app => + !data?.content?.some(item => item.applicationId === app.id) + ); + + return ( + <> + + + + + + 团队应用管理 - {teamName} + + + +
+
+ {/* 操作栏 */} +
+
+ + setSearchText(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="pl-10" + /> +
+ + {/* 多选下拉框 */} + + + + + + + + 未找到应用 + + {availableApplications.map((app) => ( + toggleAppSelection(app.id)} + className="cursor-pointer" + > + +
+ {app.appName} + {app.appCode} +
+ {selectedAppIds.includes(app.id) && ( + + )} +
+ ))} +
+
+
+
+ + {/* 批量添加按钮 */} + +
+ + {/* 表格 */} +
+
+ + + + 应用ID + 应用编码 + 应用名称 + 创建时间 + 操作 + + + + {loading ? ( + + +
+ + 加载中... +
+
+
+ ) : data?.content && data.content.length > 0 ? ( + data.content.map((record) => ( + + + {record.applicationId} + + + {record.applicationCode || '-'} + + + {record.applicationName || '-'} + + + {record.createTime || '-'} + + +
+ +
+
+
+ )) + ) : ( + + +
+ +

暂无应用

+
+
+
+ )} +
+
+
+ + {/* 分页 */} + {data && data.totalElements > 0 && ( +
+ setPageNum(newPage)} + /> +
+ )} +
+
+
+
+
+ + {/* 删除确认对话框 */} + + + + 确认删除 + + 确定要将应用 "{deleteRecord?.applicationName}" 从团队中移除吗?此操作无法撤销。 + + + + setDeleteDialogOpen(false)}> + 取消 + + + 确认删除 + + + + + + ); +}; + +export default ApplicationManageDialog; + diff --git a/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx index 5b730cd1..6831b7bd 100644 --- a/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx @@ -17,13 +17,6 @@ import { } 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, @@ -33,33 +26,36 @@ import { TableRow, } from "@/components/ui/table"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; import { useToast } from "@/components/ui/use-toast"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; import { DataTablePagination } from '@/components/ui/pagination'; import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; import { Plus, - Edit, Trash2, Search, Loader2, Users, + Check, + ChevronsUpDown, } from "lucide-react"; import { getTeamMembers, createTeamMember, - updateTeamMember, deleteTeamMember } from '../../Member/service'; -import { teamMemberFormSchema, type TeamMemberFormValues } from '../../Member/schema'; import type { TeamMemberResponse, TeamMemberQuery @@ -91,25 +87,16 @@ const MemberManageDialog: React.FC = ({ const [loading, setLoading] = useState(false); const [data, setData] = useState | null>(null); const [searchText, setSearchText] = useState(''); - const [editMode, setEditMode] = useState(false); - const [editRecord, setEditRecord] = useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteRecord, setDeleteRecord] = useState(null); + const [selectedUserIds, setSelectedUserIds] = useState([]); + const [popoverOpen, setPopoverOpen] = useState(false); + const [adding, setAdding] = useState(false); // 分页状态 const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1); const [pageSize] = useState(DEFAULT_PAGE_SIZE); - const form = useForm({ - resolver: zodResolver(teamMemberFormSchema), - defaultValues: { - teamId: teamId, - userId: undefined, - userName: '', - roleInTeam: '', - } - }); - // 加载成员列表 const loadMembers = async () => { @@ -149,28 +136,60 @@ const MemberManageDialog: React.FC = ({ loadMembers(); }; - // 新建成员 - const handleCreate = () => { - setEditRecord(null); - form.reset({ - teamId: teamId, - userId: undefined, - userName: '', - roleInTeam: '', - }); - setEditMode(true); + // 切换用户选择 + const toggleUserSelection = (userId: number) => { + setSelectedUserIds(prev => + prev.includes(userId) + ? prev.filter(id => id !== userId) + : [...prev, userId] + ); }; - // 编辑成员 - const handleEdit = (record: TeamMemberResponse) => { - setEditRecord(record); - form.reset({ - teamId: record.teamId, - userId: record.userId, - userName: record.userName || '', - roleInTeam: record.roleInTeam || '', - }); - setEditMode(true); + // 批量添加成员 + const handleBatchAdd = async () => { + if (selectedUserIds.length === 0) { + toast({ + variant: "destructive", + title: "请选择成员", + description: "至少选择一个用户进行添加", + }); + return; + } + + setAdding(true); + try { + // 并行创建所有成员关联 + await Promise.all( + selectedUserIds.map(userId => { + const user = users.find(u => u.id === userId); + return createTeamMember({ + teamId, + userId, + userName: user?.nickname || user?.username || '', + roleInTeam: '', // 批量添加时角色为空 + }); + }) + ); + + toast({ + title: "添加成功", + description: `已成功添加 ${selectedUserIds.length} 个成员`, + }); + + setSelectedUserIds([]); + setPopoverOpen(false); + loadMembers(); + onSuccess?.(); + } catch (error) { + console.error('批量添加失败:', error); + toast({ + variant: "destructive", + title: "添加失败", + description: error instanceof Error ? error.message : '未知错误', + }); + } finally { + setAdding(false); + } }; // 删除成员(打开确认对话框) @@ -203,41 +222,10 @@ const MemberManageDialog: React.FC = ({ } }; - // 保存表单 - 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(); - }; + // 过滤出未加入的用户 + const availableUsers = users.filter(user => + !data?.content?.some(member => member.userId === user.id) + ); return ( <> @@ -250,10 +238,9 @@ const MemberManageDialog: React.FC = ({ - {!editMode ? ( -
-
- {/* 搜索栏 */} +
+
+ {/* 操作栏 */}
@@ -265,9 +252,72 @@ const MemberManageDialog: React.FC = ({ className="pl-10" />
- + + + + + 未找到用户 + + {availableUsers.map((user) => ( + toggleUserSelection(user.id)} + className="cursor-pointer" + > + +
+ {user.nickname || user.username} + {user.username} +
+ {selectedUserIds.includes(user.id) && ( + + )} +
+ ))} +
+
+
+ + + {/* 批量添加按钮 */} +
@@ -280,8 +330,8 @@ const MemberManageDialog: React.FC = ({ 用户ID 用户名 团队角色 - 加入时间 - 操作 + 加入时间 + 操作 @@ -311,14 +361,6 @@ const MemberManageDialog: React.FC = ({
-
)}
-
- ) : ( -
-
- - ( - - 选择用户 - - - - )} - /> - - ( - - 团队角色 - - - - - - )} - /> - -
- - -
- - -
- )} +