From 011e13dbfa6ef318952b35afde47d719fb94ef9b Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 7 Nov 2025 11:22:26 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=89=8D=E7=AB=AF=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/ui/confirm-dialog.tsx | 227 ++++ .../components/ui/infinite-scroll-list.tsx | 106 +- .../components/ApplicationManageDialog.tsx | 68 +- .../Team/List/components/DeleteDialog.tsx | 151 ++- .../List/components/MemberManageDialog.tsx | 68 +- .../List/components/TeamApplicationDialog.tsx | 6 +- .../TeamApplicationManageDialog.tsx | 76 +- .../TeamEnvironmentManageDialog.tsx | 76 +- .../src/pages/Resource/Git/List/index.tsx | 92 +- .../src/pages/Resource/Git/List/service.ts | 4 +- .../src/pages/Resource/Jenkins/List/index.tsx | 1047 ++++++----------- .../pages/Resource/Jenkins/List/service.ts | 8 +- 12 files changed, 909 insertions(+), 1020 deletions(-) create mode 100644 frontend/src/components/ui/confirm-dialog.tsx diff --git a/frontend/src/components/ui/confirm-dialog.tsx b/frontend/src/components/ui/confirm-dialog.tsx new file mode 100644 index 00000000..eb2cfae6 --- /dev/null +++ b/frontend/src/components/ui/confirm-dialog.tsx @@ -0,0 +1,227 @@ +import React, { useState } from 'react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +export interface ConfirmDialogProps { + /** 对话框开关状态 */ + open: boolean; + /** 对话框状态变化回调 */ + onOpenChange: (open: boolean) => void; + /** 对话框标题 */ + title: string | React.ReactNode; + /** 对话框描述文字(可选) */ + description?: string | React.ReactNode; + /** 自定义内容区域(优先级高于 description) */ + children?: React.ReactNode; + + /** 传递给回调的数据项(可选) */ + item?: T; + /** 确认操作的异步函数 */ + onConfirm: (item?: T) => Promise; + /** 成功后的回调(可选) */ + onSuccess?: (item?: T) => void; + /** 失败后的回调(可选) */ + onError?: (error: any, item?: T) => void; + /** 取消操作的回调(可选) */ + onCancel?: () => void; + + /** 确认前的验证函数(返回 false 阻止提交) */ + beforeConfirm?: (item?: T) => boolean | Promise; + + /** 确认按钮文字 */ + confirmText?: string; + /** 取消按钮文字 */ + cancelText?: string; + /** 按钮样式变体 */ + variant?: 'default' | 'destructive' | 'success'; + + /** 成功后是否自动关闭(默认 true) */ + autoCloseOnSuccess?: boolean; + /** 失败后是否自动关闭(默认 true,配合 request.ts 拦截器) */ + autoCloseOnError?: boolean; + /** loading 时是否禁用取消按钮(默认 true) */ + disableCancelWhenLoading?: boolean; + + /** 自定义最大宽度 */ + maxWidth?: 'sm' | 'md' | 'lg' | 'xl'; + /** 自定义类名 */ + className?: string; +} + +/** + * 确认对话框组件 + * + * 基于 shadcn/ui 的 AlertDialog 封装,提供异步操作的完整支持 + * + * @example + * ```tsx + * // 简单删除确认 + * await deleteAPI()} + * onSuccess={() => toast({ title: '删除成功' })} + * variant="destructive" + * /> + * + * // 复杂内容 + 自定义 + * await deleteTeam(item.id)} + * variant="destructive" + * > + *
+ * + * 该团队还有成员 + * + *
+ *
+ * ``` + */ +export function ConfirmDialog({ + open, + onOpenChange, + title, + description, + children, + item, + onConfirm, + onSuccess, + onError, + onCancel, + beforeConfirm, + confirmText = '确定', + cancelText = '取消', + variant = 'default', + autoCloseOnSuccess = true, + autoCloseOnError = true, + disableCancelWhenLoading = true, + maxWidth = 'md', + className, +}: ConfirmDialogProps) { + const [loading, setLoading] = useState(false); + + /** + * 修复 Radix UI AlertDialog 关闭后 body.style.pointerEvents 残留的问题 + * 当对话框从打开变为关闭时,自动清理 body 样式 + */ + React.useEffect(() => { + if (open === false) { + // 延迟执行,确保在 AlertDialog 关闭动画之后 + const timer = setTimeout(() => { + // 强制移除 body 的 pointer-events 限制 + if (document.body.style.pointerEvents === 'none') { + document.body.style.pointerEvents = ''; + } + }, 350); // 稍微比 AlertDialog 动画时间(200ms)长一点 + + return () => clearTimeout(timer); + } + }, [open]); + + const handleConfirm = async () => { + // 前置验证 + if (beforeConfirm) { + try { + const canProceed = await beforeConfirm(item); + if (!canProceed) { + return; // 验证失败,不继续执行 + } + } catch (error) { + console.error('前置验证失败:', error); + return; + } + } + + setLoading(true); + try { + await onConfirm(item); + + // 成功后自动关闭 + if (autoCloseOnSuccess) { + onOpenChange(false); + } + + // 执行成功回调 + onSuccess?.(item); + } catch (error) { + // 失败后自动关闭(配合 request.ts 拦截器显示错误) + if (autoCloseOnError) { + onOpenChange(false); + } + + // 执行失败回调 + onError?.(error, item); + } finally { + setLoading(false); + } + }; + + const handleCancel = () => { + onCancel?.(); + }; + + // 按钮样式映射 + const variantStyles = { + default: '', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + success: 'bg-green-600 hover:bg-green-700 text-white', + }; + + // 最大宽度映射 + const maxWidthStyles = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-xl', + }; + + return ( + + + + {title} + {!children && description && ( + {description} + )} + + + {/* 自定义内容区域 */} + {children &&
{children}
} + + + + {cancelText} + + + {loading && } + {confirmText} + + +
+
+ ); +} + diff --git a/frontend/src/components/ui/infinite-scroll-list.tsx b/frontend/src/components/ui/infinite-scroll-list.tsx index 3fe309df..4ac111cc 100644 --- a/frontend/src/components/ui/infinite-scroll-list.tsx +++ b/frontend/src/components/ui/infinite-scroll-list.tsx @@ -6,7 +6,7 @@ import type { Page } from '@/types/base'; interface InfiniteScrollListProps { /** * 数据加载函数 - * @param pageNum - 页码(从0开始) + * @param pageNum - 页码(从 1 开始) * @param size - 每页大小 * @param extraParams - 额外参数 * @returns 分页数据 @@ -15,8 +15,11 @@ interface InfiniteScrollListProps { /** * 渲染单个数据项 + * @param item - 数据项 + * @param index - 索引 + * @param isSelected - 是否选中 */ - renderItem: (item: T, index: number) => React.ReactNode; + renderItem: (item: T, index: number, isSelected: boolean) => React.ReactNode; /** * 每页大小,默认 20 @@ -53,16 +56,6 @@ interface InfiniteScrollListProps { */ emptyIcon?: React.ReactNode; - /** - * 自定义空状态组件 - */ - emptyState?: React.ReactNode; - - /** - * 自定义初始加载占位符 - */ - loadingPlaceholder?: React.ReactNode; - /** * 加载完成提示文字 */ @@ -82,6 +75,33 @@ interface InfiniteScrollListProps { * 数据变化回调 */ onDataChange?: (data: T[], total: number) => void; + + /** + * 搜索关键词(外部传入,组件内部会做防抖处理) + */ + searchValue?: string; + + /** + * 本地过滤函数(对已加载的数据进行过滤) + * @param item - 数据项 + * @param searchValue - 防抖后的搜索值 + */ + localFilter?: (item: T, searchValue: string) => boolean; + + /** + * 选中的项 + */ + selectedItem?: T; + + /** + * 点击项的回调 + */ + onItemClick?: (item: T) => void; + + /** + * 用于识别项的唯一键(用于判断是否选中) + */ + itemKey?: keyof T; } export function InfiniteScrollList({ @@ -98,16 +118,32 @@ export function InfiniteScrollList({ showInitialLoading = true, resetTrigger, onDataChange, + searchValue = '', + localFilter, + selectedItem, + onItemClick, + itemKey, }: InfiniteScrollListProps) { const [data, setData] = useState([]); - const [page, setPage] = useState(0); + const [page, setPage] = useState(1); // ✅ 从 1 开始 const [hasMore, setHasMore] = useState(true); const [loading, setLoading] = useState(false); const [initialLoading, setInitialLoading] = useState(showInitialLoading); + // ✅ 搜索防抖(300ms) + const [debouncedSearchValue, setDebouncedSearchValue] = useState(searchValue); + // 使用 ref 跟踪是否正在加载,避免重复请求 const loadingRef = useRef(false); + // ✅ 搜索防抖处理 + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchValue(searchValue); + }, 300); + return () => clearTimeout(timer); + }, [searchValue]); + // 加载数据 const fetchData = useCallback(async (pageNum: number, reset: boolean = false) => { if (loadingRef.current) return; @@ -155,17 +191,29 @@ export function InfiniteScrollList({ // 重置并重新加载 const reset = useCallback(() => { setData([]); - setPage(0); + setPage(1); // ✅ 重置为 1 setHasMore(true); setInitialLoading(showInitialLoading); - fetchData(0, true); + fetchData(1, true); // ✅ 从第 1 页开始 }, [fetchData, showInitialLoading]); - // 初始加载 + // 当 resetTrigger 或 debouncedSearchValue 变化时重置 useEffect(() => { reset(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [resetTrigger]); + }, [resetTrigger, debouncedSearchValue]); + + // ✅ 本地过滤数据 + const filteredData = React.useMemo(() => { + if (!localFilter || !debouncedSearchValue) return data; + return data.filter(item => localFilter(item, debouncedSearchValue)); + }, [data, localFilter, debouncedSearchValue]); + + // ✅ 判断项是否被选中 + const isSelected = (item: T) => { + if (!selectedItem || !itemKey) return false; + return selectedItem[itemKey] === item[itemKey]; + }; // 初始 loading 状态 if (initialLoading && data.length === 0) { @@ -177,7 +225,7 @@ export function InfiniteScrollList({ } // 空状态 - if (!loading && data.length === 0) { + if (!loading && filteredData.length === 0) { return (
{emptyIcon ||
} @@ -189,7 +237,7 @@ export function InfiniteScrollList({ // 列表渲染 return ( ({
} endMessage={ - data.length > 0 ? ( + filteredData.length > 0 ? (
{endMessage}
@@ -208,15 +256,21 @@ export function InfiniteScrollList({ scrollableTarget={scrollableTarget} className={className || listClassName} > - {data.map((item, index) => ( - - {renderItem(item, index)} - - ))} + {filteredData.map((item, index) => { + const selected = isSelected(item); + return ( +
onItemClick?.(item)} + className={onItemClick ? 'cursor-pointer' : ''} + > + {renderItem(item, index, selected)} +
+ ); + })} ); } // 导出类型供外部使用 export type { InfiniteScrollListProps }; - diff --git a/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx index 725bde3a..4864e2fe 100644 --- a/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx @@ -5,16 +5,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -197,25 +188,7 @@ const ApplicationManageDialog: React.FC = ({ // 确认删除 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 : '未知错误', - }); - } + await deleteTeamApplication(deleteRecord.id); }; // 过滤出未关联的应用 @@ -401,24 +374,25 @@ const ApplicationManageDialog: React.FC = ({ {/* 删除确认对话框 */} - - - - 确认删除 - - 确定要将应用 "{deleteRecord?.applicationName}" 从团队中移除吗?此操作无法撤销。 - - - - setDeleteDialogOpen(false)}> - 取消 - - - 确认删除 - - - - + { + toast({ + title: "删除成功", + description: `应用 "${deleteRecord?.applicationName}" 已移除`, + }); + setDeleteRecord(null); + loadApplications(); + onSuccess?.(); + }} + variant="destructive" + confirmText="确认删除" + /> ); }; diff --git a/frontend/src/pages/Deploy/Team/List/components/DeleteDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/DeleteDialog.tsx index e516e417..f4e9abdf 100644 --- a/frontend/src/pages/Deploy/Team/List/components/DeleteDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/DeleteDialog.tsx @@ -1,15 +1,7 @@ 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 { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { deleteTeam } from '../service'; import type { TeamResponse } from '../types'; @@ -33,92 +25,83 @@ const DeleteDialog: React.FC = ({ 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' - }); - } + await deleteTeam(record.id); }; if (!record) return null; return ( - - - - - - 确认删除 - - - 此操作将永久删除该团队,该操作无法撤销。 - - - -
-
-
- 团队编码: - - {record.teamCode} - -
-
- 团队名称: - {record.teamName} -
- {record.ownerName && ( -
- 负责人: - {record.ownerName} -
- )} - {record.memberCount !== undefined && record.memberCount > 0 && ( -
- 成员数量: - {record.memberCount} 人 -
- )} - {record.applicationCount !== undefined && record.applicationCount > 0 && ( -
- 关联应用: - {record.applicationCount} 个 -
- )} + + + 确认删除 +
+ } + description="此操作将永久删除该团队,该操作无法撤销。" + item={record} + onConfirm={handleConfirm} + onSuccess={() => { + toast({ + title: '删除成功', + description: `团队 "${record.teamName}" 已删除` + }); + onSuccess(); + }} + variant="destructive" + confirmText="确认删除" + maxWidth="md" + > +
+
+
+ 团队编码: + + {record.teamCode} +
- - {((record.memberCount !== undefined && record.memberCount > 0) || - (record.applicationCount !== undefined && record.applicationCount > 0)) && ( -
- -

- 警告:该团队还有{record.memberCount || 0}名成员和{record.applicationCount || 0}个关联应用,删除后将影响这些关联数据。 -

+
+ 团队名称: + {record.teamName} +
+ {record.ownerName && ( +
+ 负责人: + {record.ownerName} +
+ )} + {record.memberCount !== undefined && record.memberCount > 0 && ( +
+ 成员数量: + + {record.memberCount} 人 + +
+ )} + {record.applicationCount !== undefined && record.applicationCount > 0 && ( +
+ 关联应用: + + {record.applicationCount} 个 +
)}
- - - - - -
+ {((record.memberCount !== undefined && record.memberCount > 0) || + (record.applicationCount !== undefined && record.applicationCount > 0)) && ( +
+ +

+ 警告:该团队还有{record.memberCount || 0}名成员和{record.applicationCount || 0}个关联应用,删除后将影响这些关联数据。 +

+
+ )} +
+
); }; export default DeleteDialog; - diff --git a/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx index b5e7a7a5..75f1f424 100644 --- a/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx @@ -5,16 +5,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -245,25 +236,7 @@ const MemberManageDialog: React.FC = ({ // 确认删除 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 : '未知错误', - }); - } + await deleteTeamMember(deleteRecord.id); }; // 过滤出未加入的用户(排除负责人和已加入的成员) @@ -488,24 +461,25 @@ const MemberManageDialog: React.FC = ({ {/* 删除确认对话框 */} - - - - 确认删除 - - 确定要将成员 "{deleteRecord?.userName}" 从团队中移除吗?此操作无法撤销。 - - - - setDeleteDialogOpen(false)}> - 取消 - - - 确认删除 - - - - + { + toast({ + title: "删除成功", + description: `成员 "${deleteRecord?.userName}" 已移除`, + }); + setDeleteRecord(null); + loadMembers(); + onSuccess?.(); + }} + variant="destructive" + confirmText="确认删除" + /> ); }; diff --git a/frontend/src/pages/Deploy/Team/List/components/TeamApplicationDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/TeamApplicationDialog.tsx index 7e6f22e5..4cfa2bbc 100644 --- a/frontend/src/pages/Deploy/Team/List/components/TeamApplicationDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/TeamApplicationDialog.tsx @@ -243,7 +243,7 @@ const TeamApplicationDialog: React.FC = ({ {/* 应用选择 */}
diff --git a/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx index e992bc9e..0516db0c 100644 --- a/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx @@ -15,16 +15,7 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/use-toast'; import { Plus, Edit, Trash2, Loader2 } from 'lucide-react'; @@ -76,7 +67,6 @@ export const TeamApplicationManageDialog: React.FC< // 删除确认对话框状态 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deletingApp, setDeletingApp] = useState(null); - const [deleting, setDeleting] = useState(false); // 加载基础数据和应用列表 useEffect(() => { @@ -196,27 +186,7 @@ export const TeamApplicationManageDialog: React.FC< const handleConfirmDelete = async () => { if (!deletingApp) return; - - setDeleting(true); - try { - await deleteTeamApplication(deletingApp.id!); - toast({ - title: '删除成功', - description: '应用配置已删除', - }); - setDeleteDialogOpen(false); - setDeletingApp(null); - loadTeamApplications(); - onSuccess?.(); - } catch (error: any) { - toast({ - title: '删除失败', - description: error.message || '删除应用配置失败', - variant: 'destructive', - }); - } finally { - setDeleting(false); - } + await deleteTeamApplication(deletingApp.id!); }; const getEnvironmentName = (environmentId: number) => { @@ -346,29 +316,25 @@ export const TeamApplicationManageDialog: React.FC< )} {/* 删除确认对话框 */} - - - - 确认删除 - - 确定要删除应用配置 " - {deletingApp?.applicationName || `应用 ${deletingApp?.applicationId}`}" 吗? - 此操作无法撤销。 - - - - 取消 - - {deleting && } - 删除 - - - - + { + toast({ + title: '删除成功', + description: '应用配置已删除', + }); + setDeletingApp(null); + loadTeamApplications(); + onSuccess?.(); + }} + variant="destructive" + confirmText="删除" + /> ); }; diff --git a/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentManageDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentManageDialog.tsx index 27010c01..c76020cd 100644 --- a/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentManageDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentManageDialog.tsx @@ -15,16 +15,7 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { useToast } from '@/components/ui/use-toast'; @@ -85,7 +76,6 @@ export const TeamEnvironmentManageDialog: React.FC< const [deletingConfig, setDeletingConfig] = useState< TeamEnvironmentConfig | null >(null); - const [deleting, setDeleting] = useState(false); // 加载团队的环境配置列表 useEffect(() => { @@ -139,28 +129,7 @@ export const TeamEnvironmentManageDialog: React.FC< const handleConfirmDelete = async () => { if (!deletingConfig) return; - - setDeleting(true); - try { - await deleteTeamEnvironmentConfig(deletingConfig.id); - - toast({ - title: '删除成功', - description: '环境配置已删除', - }); - setDeleteDialogOpen(false); - setDeletingConfig(null); - loadEnvironmentConfigs(); - onSuccess?.(); - } catch (error: any) { - toast({ - title: '删除失败', - description: error.message || '删除环境配置失败', - variant: 'destructive', - }); - } finally { - setDeleting(false); - } + await deleteTeamEnvironmentConfig(deletingConfig.id); }; const handleConfigSuccess = () => { @@ -365,28 +334,25 @@ export const TeamEnvironmentManageDialog: React.FC< )} {/* 删除确认对话框 */} - - - - 确认删除 - - 确定要删除环境"{deletingConfig?.environmentName}"的配置吗? - 此操作将同时删除该环境下的所有应用配置,且无法撤销。 - - - - 取消 - - {deleting && } - 删除 - - - - + { + toast({ + title: '删除成功', + description: '环境配置已删除', + }); + setDeletingConfig(null); + loadEnvironmentConfigs(); + onSuccess?.(); + }} + variant="destructive" + confirmText="删除" + /> ); }; diff --git a/frontend/src/pages/Resource/Git/List/index.tsx b/frontend/src/pages/Resource/Git/List/index.tsx index c97920f5..2881c77b 100644 --- a/frontend/src/pages/Resource/Git/List/index.tsx +++ b/frontend/src/pages/Resource/Git/List/index.tsx @@ -43,7 +43,7 @@ import { Calendar, User, GitCommit, - Download, + RotateCw, } from 'lucide-react'; import type { RepositoryGroupResponse, @@ -111,6 +111,13 @@ const GitManager: React.FC = () => { branches: false, }); + // 数量统计 + const [projectCount, setProjectCount] = useState(0); + const [branchCount, setBranchCount] = useState(0); + + // 刷新触发器 + const [projectRefreshKey, setProjectRefreshKey] = useState(0); + const [branchRefreshKey, setBranchRefreshKey] = useState(0); // 搜索过滤 - 仅保留仓库组搜索 const [groupSearch, setGroupSearch] = useState(''); @@ -193,9 +200,6 @@ const GitManager: React.FC = () => { } }, [selectedInstanceId, buildTree, toast]); - // 加载项目列表(支持不传repoGroupId加载所有项目)- 现在不再需要,由 InfiniteScrollList 处理 - // 加载分支列表(支持不传repoProjectId加载该组所有分支)- 现在不再需要,由 InfiniteScrollList 处理 - // 同步所有数据(异步任务) const handleSyncAll = useCallback(async () => { if (!selectedInstanceId) return; @@ -462,8 +466,6 @@ const GitManager: React.FC = () => { ); }; - // 项目和分支列表现在由 InfiniteScrollList 组件内部管理,不再需要客户端过滤 - // 加载 Git 实例列表(只在组件挂载时执行一次) useEffect(() => { const loadInstancesEffect = async () => { @@ -513,6 +515,17 @@ const GitManager: React.FC = () => { const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, filterGroupTree]); + // 缓存 resetTrigger 避免不必要的重置 + const projectResetTrigger = React.useMemo( + () => [selectedInstanceId, selectedGroup?.id, projectRefreshKey], + [selectedInstanceId, selectedGroup?.id, projectRefreshKey] + ); + + const branchResetTrigger = React.useMemo( + () => [selectedInstanceId, selectedProject?.id, branchRefreshKey], + [selectedInstanceId, selectedProject?.id, branchRefreshKey] + ); + return ( @@ -576,6 +589,7 @@ const GitManager: React.FC = () => { className="h-7 w-7" onClick={loadGroupTree} disabled={loading.groups || !selectedInstanceId} + title="刷新列表" > { className="h-7 w-7" onClick={handleSyncGroups} disabled={syncing.groups || !selectedInstanceId} + title="从Git同步" > -
@@ -632,22 +647,33 @@ const GitManager: React.FC = () => {
- - 项目{selectedGroup && ` - ${selectedGroup.name}`} + + 项目 ({projectCount}){selectedGroup && ` - ${selectedGroup.name}`}
- + -
+ +
{ size, }) } + onDataChange={(data, total) => setProjectCount(data.length)} renderItem={(project) => (
{ )} scrollableTarget="projectScrollableDiv" pageSize={20} - resetTrigger={selectedGroup?.id} + resetTrigger={projectResetTrigger} emptyText={selectedGroup ? '当前仓库组暂无项目' : '暂无项目'} className="p-2 space-y-2" /> @@ -745,22 +772,33 @@ const GitManager: React.FC = () => {
- - 分支{selectedProject && ` - ${selectedProject.name}`} + + 分支 ({branchCount}){selectedProject && ` - ${selectedProject.name}`}
- + -
+ +
{ size, }) } + onDataChange={(data, total) => setBranchCount(data.length)} renderItem={(branch) => (
@@ -845,7 +884,7 @@ const GitManager: React.FC = () => { )} scrollableTarget="branchScrollableDiv" pageSize={20} - resetTrigger={selectedProject?.id} + resetTrigger={branchResetTrigger} emptyText={selectedProject ? '当前项目暂无分支' : '暂无分支'} className="" /> @@ -859,4 +898,3 @@ const GitManager: React.FC = () => { }; export default GitManager; - diff --git a/frontend/src/pages/Resource/Git/List/service.ts b/frontend/src/pages/Resource/Git/List/service.ts index 483579b1..82fbdb71 100644 --- a/frontend/src/pages/Resource/Git/List/service.ts +++ b/frontend/src/pages/Resource/Git/List/service.ts @@ -54,7 +54,7 @@ export const getRepositoryProjects = (params: { }) => request.get>(`${PROJECT_URL}/page`, { params: { - pageNum: params.pageNum ?? 0, + pageNum: params.pageNum ?? 1, size: params.size ?? 20, externalSystemId: params.externalSystemId, repoGroupId: params.repoGroupId, @@ -84,7 +84,7 @@ export const getRepositoryBranches = (params: { }) => request.get>(`${BRANCH_URL}/page`, { params: { - pageNum: params.pageNum ?? 0, + pageNum: params.pageNum ?? 1, size: params.size ?? 20, sortField: 'lastUpdateTime', sortOrder: 'desc', diff --git a/frontend/src/pages/Resource/Jenkins/List/index.tsx b/frontend/src/pages/Resource/Jenkins/List/index.tsx index 2f8637ed..b2ccbe74 100644 --- a/frontend/src/pages/Resource/Jenkins/List/index.tsx +++ b/frontend/src/pages/Resource/Jenkins/List/index.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; -import InfiniteScroll from 'react-infinite-scroll-component'; +import React, { useState, useEffect, useCallback } from 'react'; +import { InfiniteScrollList } from '@/components/ui/infinite-scroll-list'; import { Card, CardHeader, @@ -38,7 +38,7 @@ import { AlertTriangle, Calendar, Activity, - Download, + RotateCw, } from 'lucide-react'; import type { JenkinsInstance, @@ -85,31 +85,19 @@ const JenkinsManager: React.FC = () => { // 数据状态 const [views, setViews] = useState([]); - const [jobs, setJobs] = useState([]); - const [builds, setBuilds] = useState([]); - - // 分页状态 - const [jobPage, setJobPage] = useState(0); - const [jobHasMore, setJobHasMore] = useState(true); - const [buildPage, setBuildPage] = useState(0); - const [buildHasMore, setBuildHasMore] = useState(true); // 选中状态 const [selectedView, setSelectedView] = useState(); const [selectedJob, setSelectedJob] = useState(); // 加载状态 - const [loading, setLoading] = useState({ - views: false, - jobs: false, - builds: false, - }); + const [loading, setLoading] = useState({ views: false }); // 同步状态 const [syncing, setSyncing] = useState({ - views: false, - jobs: false, - builds: false, + views: false, + jobs: false, + builds: false, }); // 搜索状态 @@ -117,39 +105,13 @@ const JenkinsManager: React.FC = () => { const [jobSearch, setJobSearch] = useState(''); const [buildSearch, setBuildSearch] = useState(''); - // 防抖搜索值 - 优化大量数据过滤性能 - const [debouncedViewSearch, setDebouncedViewSearch] = useState(''); - const [debouncedJobSearch, setDebouncedJobSearch] = useState(''); - const [debouncedBuildSearch, setDebouncedBuildSearch] = useState(''); + // 数量统计 + const [jobCount, setJobCount] = useState(0); + const [buildCount, setBuildCount] = useState(0); - // 使用 ref 跟踪初始化状态,避免重复请求 - const isInitializedRef = useRef(false); - const viewsLoadedRef = useRef(false); - const jobsLoadedRef = useRef(false); - - // 视图搜索防抖 - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedViewSearch(viewSearch); - }, 300); - return () => clearTimeout(timer); - }, [viewSearch]); - - // 任务搜索防抖 - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedJobSearch(jobSearch); - }, 300); - return () => clearTimeout(timer); - }, [jobSearch]); - - // 构建搜索防抖(特别重要,因为有5000+条数据) - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedBuildSearch(buildSearch); - }, 300); - return () => clearTimeout(timer); - }, [buildSearch]); + // 刷新触发器 + const [jobRefreshKey, setJobRefreshKey] = useState(0); + const [buildRefreshKey, setBuildRefreshKey] = useState(0); // 加载视图列表 const loadViews = useCallback(async () => { @@ -169,146 +131,35 @@ const JenkinsManager: React.FC = () => { } }, [selectedInstanceId, toast]); - // 加载任务列表(首次加载或重置) - const loadJobs = useCallback(async (viewId?: number, reset: boolean = true) => { - if (!selectedInstanceId) return; - setLoading((prev) => ({ ...prev, jobs: true })); - try { - const pageNum = reset ? 0 : jobPage; - const response = await getJenkinsJobs({ - externalSystemId: selectedInstanceId, - viewId, - pageNum, - size: 20, - }); - if (response) { - if (reset) { - setJobs(response.content || []); - setJobPage(0); - } else { - setJobs((prev) => [...prev, ...(response.content || [])]); - } - setJobHasMore(!response.last); - } - } catch (error) { - toast({ - variant: 'destructive', - title: '加载失败', - description: '获取任务列表失败', - }); - } finally { - setLoading((prev) => ({ ...prev, jobs: false })); - } - }, [selectedInstanceId, jobPage, toast]); - - // 加载更多任务 - const loadMoreJobs = useCallback(async () => { - if (!selectedInstanceId || !jobHasMore || loading.jobs) return; - setLoading((prev) => ({ ...prev, jobs: true })); - try { - const nextPage = jobPage + 1; - const response = await getJenkinsJobs({ - externalSystemId: selectedInstanceId, - viewId: selectedView?.id, - pageNum: nextPage, - size: 20, - }); - if (response) { - setJobs((prev) => [...prev, ...(response.content || [])]); - setJobPage(nextPage); - setJobHasMore(!response.last); - } - } catch (error) { - toast({ - variant: 'destructive', - title: '加载失败', - description: '获取更多任务失败', - }); - } finally { - setLoading((prev) => ({ ...prev, jobs: false })); - } - }, [selectedInstanceId, selectedView, jobPage, jobHasMore, loading.jobs, toast]); - - // 加载构建列表(首次加载或重置) - const loadBuilds = useCallback(async (jobId?: number, reset: boolean = true) => { - if (!selectedInstanceId) return; - setLoading((prev) => ({ ...prev, builds: true })); - try { - const pageNum = reset ? 0 : buildPage; - const response = await getJenkinsBuilds({ - externalSystemId: selectedInstanceId, - jobId, - pageNum, - size: 20, - }); - if (response) { - if (reset) { - setBuilds(response.content || []); - setBuildPage(0); - } else { - setBuilds((prev) => [...prev, ...(response.content || [])]); - } - setBuildHasMore(!response.last); - } - } catch (error) { - toast({ - variant: 'destructive', - title: '加载失败', - description: '获取构建列表失败', - }); - } finally { - setLoading((prev) => ({ ...prev, builds: false })); - } - }, [selectedInstanceId, buildPage, toast]); - - // 加载更多构建 - const loadMoreBuilds = useCallback(async () => { - if (!selectedInstanceId || !buildHasMore || loading.builds) return; - setLoading((prev) => ({ ...prev, builds: true })); - try { - const nextPage = buildPage + 1; - const response = await getJenkinsBuilds({ - externalSystemId: selectedInstanceId, - jobId: selectedJob?.id, - pageNum: nextPage, - size: 20, - }); - if (response) { - setBuilds((prev) => [...prev, ...(response.content || [])]); - setBuildPage(nextPage); - setBuildHasMore(!response.last); - } - } catch (error) { - toast({ - variant: 'destructive', - title: '加载失败', - description: '获取更多构建失败', - }); - } finally { - setLoading((prev) => ({ ...prev, builds: false })); - } - }, [selectedInstanceId, selectedJob, buildPage, buildHasMore, loading.builds, toast]); - - // 加载 Jenkins 实例列表(只在组件挂载时执行一次) + // 加载 Jenkins 实例列表 useEffect(() => { const loadInstances = async () => { - try { - const data = await getJenkinsInstances(); + try { + const data = await getJenkinsInstances(); setInstances(data); if (data.length > 0 && !selectedInstanceId) { setSelectedInstanceId(data[0].id); - } - } catch (error) { - toast({ - variant: 'destructive', + } + } catch (error) { + toast({ + variant: 'destructive', title: '加载失败', description: '获取 Jenkins 实例列表失败', - }); - } - }; + }); + } + }; loadInstances(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // 只在组件挂载时执行一次 + }, []); + + // 实例变化时加载视图 + useEffect(() => { + if (!selectedInstanceId) return; + setSelectedView(undefined); + setSelectedJob(undefined); + loadViews(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedInstanceId]); // 同步视图 const handleSyncViews = useCallback(async () => { @@ -316,18 +167,18 @@ const JenkinsManager: React.FC = () => { setSyncing((prev) => ({ ...prev, views: true })); try { await syncJenkinsViews(selectedInstanceId); - toast({ - title: '同步成功', + toast({ + title: '同步成功', description: '视图同步任务已启动', - }); + }); setTimeout(() => loadViews(), 2000); - } catch (error) { - toast({ - variant: 'destructive', - title: '同步失败', + } catch (error) { + toast({ + variant: 'destructive', + title: '同步失败', description: '视图同步失败', - }); - } finally { + }); + } finally { setSyncing((prev) => ({ ...prev, views: false })); } }, [selectedInstanceId, toast, loadViews]); @@ -341,24 +192,20 @@ const JenkinsManager: React.FC = () => { externalSystemId: selectedInstanceId, viewId: selectedView?.id, }); - toast({ - title: '同步成功', + toast({ + title: '同步成功', description: selectedView ? '视图任务同步任务已启动' : '所有任务同步任务已启动', - }); - setTimeout(() => { - // 无论是否选择视图,都重新加载任务列表 - loadJobs(selectedView?.id); - }, 2000); - } catch (error) { - toast({ - variant: 'destructive', - title: '同步失败', + }); + } catch (error) { + toast({ + variant: 'destructive', + title: '同步失败', description: '任务同步失败', - }); - } finally { + }); + } finally { setSyncing((prev) => ({ ...prev, jobs: false })); } - }, [selectedInstanceId, selectedView, toast, loadJobs]); + }, [selectedInstanceId, selectedView, toast]); // 同步构建 const handleSyncBuilds = useCallback(async () => { @@ -369,132 +216,34 @@ const JenkinsManager: React.FC = () => { externalSystemId: selectedInstanceId, jobId: selectedJob?.id, }); - toast({ + toast({ title: '同步成功', description: selectedJob ? '任务构建同步任务已启动' : '所有构建同步任务已启动', - }); - setTimeout(() => { - // 无论是否选择任务,都重新加载构建列表 - loadBuilds(selectedJob?.id); - }, 2000); - } catch (error) { - toast({ - variant: 'destructive', - title: '同步失败', + }); + } catch (error) { + toast({ + variant: 'destructive', + title: '同步失败', description: '构建同步失败', - }); - } finally { + }); + } finally { setSyncing((prev) => ({ ...prev, builds: false })); - } - }, [selectedInstanceId, selectedJob, toast, loadBuilds]); - - // 实例变化时 - 重置状态并加载视图 - useEffect(() => { - if (!selectedInstanceId) return; - - // 重置所有状态和标志 - isInitializedRef.current = false; - viewsLoadedRef.current = false; - jobsLoadedRef.current = false; - - setSelectedView(undefined); - setSelectedJob(undefined); - setJobs([]); - setBuilds([]); - setJobPage(0); - setJobHasMore(true); - setBuildPage(0); - setBuildHasMore(true); - - // 加载视图列表 - loadViews(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedInstanceId]); // 只监听 selectedInstanceId,避免重复请求 - - // 视图加载完成后 - 自动加载任务(只执行一次) - useEffect(() => { - if (selectedInstanceId && views.length > 0 && !viewsLoadedRef.current) { - viewsLoadedRef.current = true; - loadJobs(undefined, true); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedInstanceId, views.length]); // 监听 views.length 变化 + }, [selectedInstanceId, selectedJob, toast]); - // 任务加载完成后 - 自动加载构建(只执行一次) - useEffect(() => { - if (selectedInstanceId && jobs.length > 0 && !jobsLoadedRef.current) { - jobsLoadedRef.current = true; - isInitializedRef.current = true; // 标记初始化完成 - loadBuilds(undefined, true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedInstanceId, jobs.length]); // 监听 jobs.length 变化 - - // 视图选择变化 - 重新加载任务 - useEffect(() => { - // 只有初始化完成后才处理选择变化 - if (!isInitializedRef.current) return; - - // 立即清空旧数据,避免渲染大量过时数据导致卡顿 - setSelectedJob(undefined); - setJobs([]); // 🔥 关键:立即清空任务数据 - setBuilds([]); - setJobPage(0); - setJobHasMore(true); - setBuildPage(0); - setBuildHasMore(true); - - // 重新加载任务 - loadJobs(selectedView?.id, true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedView?.id]); // 只监听 selectedView?.id - - // 任务选择变化 - 重新加载构建 - useEffect(() => { - // 只有初始化完成后才处理选择变化 - if (!isInitializedRef.current) return; - - // 立即清空旧数据,避免渲染大量过时数据导致卡顿 - setBuilds([]); // 🔥 关键:立即清空构建数据 - setBuildPage(0); - setBuildHasMore(true); - - // 重新加载构建 - loadBuilds(selectedJob?.id, true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedJob?.id]); // 只监听 selectedJob?.id - - // 过滤视图(使用防抖搜索值) - const filteredViews = useMemo(() => { - if (!debouncedViewSearch) return views; + // 过滤视图 + const filteredViews = React.useMemo(() => { + if (!viewSearch) return views; return views.filter((view) => - view.viewName.toLowerCase().includes(debouncedViewSearch.toLowerCase()) + view.viewName.toLowerCase().includes(viewSearch.toLowerCase()) ); - }, [views, debouncedViewSearch]); - - // 过滤任务(使用防抖搜索值) - const filteredJobs = useMemo(() => { - if (!debouncedJobSearch) return jobs; - return jobs.filter((job) => - job.jobName.toLowerCase().includes(debouncedJobSearch.toLowerCase()) - ); - }, [jobs, debouncedJobSearch]); - - // 过滤构建(使用防抖搜索值 - 特别重要,5000+条数据) - const filteredBuilds = useMemo(() => { - if (!debouncedBuildSearch) return builds; - const searchLower = debouncedBuildSearch.toLowerCase(); - return builds.filter((build) => - build.buildNumber.toString().includes(searchLower) || - build.buildStatus.toLowerCase().includes(searchLower) - ); - }, [builds, debouncedBuildSearch]); + }, [views, viewSearch]); // 获取构建状态徽章 const getBuildStatusBadge = useCallback((status: string) => { const config = BUILD_STATUS_CONFIG[status] || BUILD_STATUS_CONFIG.NOT_BUILT; const Icon = config.icon; - return ( + return ( {config.label} @@ -513,228 +262,201 @@ const JenkinsManager: React.FC = () => { return `${hours}小时${minutes % 60}分钟`; }, []); + // 缓存 resetTrigger 避免不必要的重置 + const jobResetTrigger = React.useMemo( + () => [selectedInstanceId, selectedView?.id, jobRefreshKey], + [selectedInstanceId, selectedView?.id, jobRefreshKey] + ); - return ( + const buildResetTrigger = React.useMemo( + () => [selectedInstanceId, selectedJob?.id, buildRefreshKey], + [selectedInstanceId, selectedJob?.id, buildRefreshKey] + ); + + return ( -
- {/* 页面标题 */} -
-
-

Jenkins 管理

-

- 管理和监控 Jenkins 视图、任务和构建。 -

+
+ {/* 页面标题 */} +
+
+

Jenkins 管理

+

+ 管理和监控 Jenkins 视图、任务和构建。 +

+
+
- -
- {/* 三栏布局 */} -
- {/* 左栏:视图列表 */} - - -
- - - 视图 ({filteredViews.length}) - -
- - -
-
-
- - setViewSearch(e.target.value)} - className="pl-8 h-8" - /> -
-
- -
- {loading.views ? ( -
- -
- ) : filteredViews.length === 0 ? ( -
- -

暂无视图

-
- ) : ( - filteredViews.map((view) => ( -
setSelectedView(selectedView?.id === view.id ? undefined : view)} + {/* 三栏布局 */} +
+ {/* 左栏:视图列表 */} + + +
+ + + 视图 ({filteredViews.length}) + +
+ - - - -

在 Jenkins 中查看

-
- + + + +
+
+
+ + setViewSearch(e.target.value)} + className="pl-8 h-8" + /> +
+
+ +
+ {loading.views ? ( +
+ +
+ ) : filteredViews.length === 0 ? ( +
+ +

暂无视图

+
+ ) : ( + filteredViews.map((view) => ( +
setSelectedView(selectedView?.id === view.id ? undefined : view)} + > +
+ {view.viewName} + {view.jobCount !== undefined && view.jobCount > 0 && ( + + {view.jobCount} + + )} + {view.viewUrl && ( + + + e.stopPropagation()} + className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" + > + + + + +

在 Jenkins 中查看

+
+
+ )} +
+ {view.description && ( +

+ {view.description} +

)}
- {view.description && ( -

- {view.description} -

- )} -
- )) - )} -
- - + )) + )} +
+ + - {/* 中栏:任务列表 */} - - -
- - 任务 ({filteredJobs.length}){selectedView && ` - ${selectedView.viewName}`} - -
- - -
-
-
- - setJobSearch(e.target.value)} - className="pl-8 h-8" - /> -
-
-
- {loading.jobs && filteredJobs.length === 0 ? ( -
- -
- ) : filteredJobs.length === 0 ? ( -
- -

{selectedView ? '当前视图暂无任务' : '暂无任务'}

-
- ) : ( - - - 加载更多... -
- } - endMessage={ -
- 已加载全部任务 -
- } - scrollableTarget="jobScrollableDiv" - className="p-2 space-y-2" - > - {filteredJobs.map((job) => ( -
setSelectedJob(selectedJob?.id === job.id ? undefined : job)} + {/* 中栏:任务列表 */} + + +
+ + 任务 ({jobCount}){selectedView && ` - ${selectedView.viewName}`} + +
+ + +
+
+
+ + setJobSearch(e.target.value)} + className="pl-8 h-8" + /> +
+
+
+ + loadData={(pageNum, size) => getJenkinsJobs({ + externalSystemId: selectedInstanceId!, + viewId: selectedView?.id, + pageNum, + size, + })} + onDataChange={(data, total) => setJobCount(data.length)} + renderItem={(job, index, isSelected) => ( +
{job.jobName} {job.buildCount !== undefined && job.buildCount > 0 && ( @@ -754,7 +476,7 @@ const JenkinsManager: React.FC = () => { > + @@ -762,14 +484,12 @@ const JenkinsManager: React.FC = () => { )} -
- +
{job.description && (

{job.description}

)} -
{job.lastBuildStatus && getBuildStatusBadge(job.lastBuildStatus)} {job.healthReportScore !== undefined && ( @@ -777,10 +497,7 @@ const JenkinsManager: React.FC = () => {
- + {job.healthReportScore}% @@ -791,8 +508,7 @@ const JenkinsManager: React.FC = () => { )} -
- +
{job.lastBuildTime && (
@@ -800,142 +516,131 @@ const JenkinsManager: React.FC = () => {
)}
- ))} - - )} -
-
- - {/* 右栏:构建列表 */} - - -
- - 构建 ({filteredBuilds.length}){selectedJob && ` - ${selectedJob.jobName}`} - -
- - -
-
-
- - setBuildSearch(e.target.value)} - className="pl-8 h-8" - /> -
-
-
- {loading.builds && filteredBuilds.length === 0 ? ( -
- -
- ) : filteredBuilds.length === 0 ? ( -
- -

{selectedJob ? '当前任务暂无构建' : '暂无构建'}

+ )} + searchValue={jobSearch} + localFilter={(job, search) => + job.jobName.toLowerCase().includes(search.toLowerCase()) + } + selectedItem={selectedJob} + onItemClick={(job) => setSelectedJob(selectedJob?.id === job.id ? undefined : job)} + itemKey="id" + scrollableTarget="jobScrollableDiv" + emptyText={selectedView ? '当前视图暂无任务' : '暂无任务'} + emptyIcon={} + resetTrigger={jobResetTrigger} + />
- ) : ( - - - 加载更多... + + + {/* 右栏:构建列表 */} + + +
+ + 构建 ({buildCount}){selectedJob && ` - ${selectedJob.jobName}`} + +
+ + +
+
+
+ + setBuildSearch(e.target.value)} + className="pl-8 h-8" + /> +
+
+
+ + loadData={(pageNum, size) => getJenkinsBuilds({ + externalSystemId: selectedInstanceId!, + jobId: selectedJob?.id, + pageNum, + size, + })} + onDataChange={(data, total) => setBuildCount(data.length)} + renderItem={(build) => ( +
+
+
+ + #{build.buildNumber} + + {getBuildStatusBadge(build.buildStatus)} +
+ {build.buildUrl && ( + + + + + + + +

在 Jenkins 中查看

+
+
+ )} +
+ {build.starttime && ( +
+ + {dayjs(build.starttime).format('YYYY-MM-DD HH:mm:ss')} +
+ )} + {build.duration !== undefined && ( +
+ + 耗时: {formatDuration(build.duration)} +
+ )}
- } - endMessage={ -
- 已加载全部构建 -
- } + )} + searchValue={buildSearch} + localFilter={(build, search) => { + const searchLower = search.toLowerCase(); + return build.buildNumber.toString().includes(searchLower) || + build.buildStatus.toLowerCase().includes(searchLower); + }} scrollableTarget="buildScrollableDiv" - className="p-2 space-y-2" - > - {filteredBuilds.map((build) => ( -
-
-
- - #{build.buildNumber} - - {getBuildStatusBadge(build.buildStatus)} -
- {build.buildUrl && ( - - - - - - - -

在 Jenkins 中查看

-
-
- )} + emptyText={selectedJob ? '当前任务暂无构建' : '暂无构建'} + emptyIcon={} + resetTrigger={buildResetTrigger} + /> +
+
- - {build.starttime && ( -
- - {dayjs(build.starttime).format('YYYY-MM-DD HH:mm:ss')} -
- )} - - {build.duration !== undefined && ( -
- - 耗时: {formatDuration(build.duration)} -
- )}
- ))} -
- )} -
-
-
-
- ); + ); }; export default JenkinsManager; diff --git a/frontend/src/pages/Resource/Jenkins/List/service.ts b/frontend/src/pages/Resource/Jenkins/List/service.ts index 52db0dd5..1660d452 100644 --- a/frontend/src/pages/Resource/Jenkins/List/service.ts +++ b/frontend/src/pages/Resource/Jenkins/List/service.ts @@ -15,7 +15,7 @@ export const getJenkinsInstances = () => // 获取视图列表 export const getJenkinsViews = (externalSystemId: number) => - request.get(`/api/v1/jenkins-view`, { + request.get(`/api/v1/jenkins-view/list`, { params: { externalSystemId } }); @@ -28,6 +28,7 @@ export const syncJenkinsViews = (externalSystemId: number) => // ==================== Jenkins 任务 ==================== // 获取任务列表(分页) +// 注意:pageNum 从 1 开始,后端会自动转换为从 0 开始给 Spring Data JPA export const getJenkinsJobs = (params: { externalSystemId: number; viewId?: number; @@ -36,7 +37,7 @@ export const getJenkinsJobs = (params: { }) => request.get>(`/api/v1/jenkins-job/page`, { params: { - pageNum: params.pageNum ?? 0, + pageNum: params.pageNum ?? 1, size: params.size ?? 20, externalSystemId: params.externalSystemId, viewId: params.viewId @@ -50,6 +51,7 @@ export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: num // ==================== Jenkins 构建 ==================== // 获取构建列表(分页,按开始时间倒序排序) +// 注意:pageNum 从 1 开始,后端会自动转换为从 0 开始给 Spring Data JPA export const getJenkinsBuilds = (params: { externalSystemId: number; jobId?: number; @@ -58,7 +60,7 @@ export const getJenkinsBuilds = (params: { }) => request.get>(`/api/v1/jenkins-build/page`, { params: { - pageNum: params.pageNum ?? 0, + pageNum: params.pageNum ?? 1, size: params.size ?? 20, sortField: 'starttime', sortOrder: 'desc',