From e087b6abd8b05d26fe80a5f6a237cd2ca2044907 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 30 Oct 2025 10:18:23 +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 --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 20 + .../pages/Deploy/GitManager/List/index.tsx | 354 ++++++----- .../Deploy/JenkinsManager/List/index.tsx | 594 ++++++++++-------- .../Deploy/JenkinsManager/List/service.ts | 4 +- 5 files changed, 569 insertions(+), 404 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 21e545c6..66f1203f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,6 +42,7 @@ "@react-form-builder/designer": "^7.4.0", "@react-form-builder/designer-bundle": "^7.4.0", "@reduxjs/toolkit": "^2.0.1", + "@tanstack/react-virtual": "^3.13.12", "@types/recharts": "^1.8.29", "@types/uuid": "^10.0.0", "@xyflow/react": "^12.8.6", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 6ec0539f..95066b46 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: '@reduxjs/toolkit': specifier: ^2.0.1 version: 2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + '@tanstack/react-virtual': + specifier: ^3.13.12 + version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/recharts': specifier: ^1.8.29 version: 1.8.29 @@ -1950,6 +1953,15 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@tanstack/react-virtual@3.13.12': + resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -6167,6 +6179,14 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/virtual-core@3.13.12': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.3 diff --git a/frontend/src/pages/Deploy/GitManager/List/index.tsx b/frontend/src/pages/Deploy/GitManager/List/index.tsx index 1c5b9b15..eafe28db 100644 --- a/frontend/src/pages/Deploy/GitManager/List/index.tsx +++ b/frontend/src/pages/Deploy/GitManager/List/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { Card, CardHeader, @@ -43,6 +43,7 @@ import { Calendar, User, GitCommit, + Download, } from 'lucide-react'; import type { RepositoryGroupResponse, @@ -116,34 +117,39 @@ const GitManager: React.FC = () => { // 搜索过滤 const [groupSearch, setGroupSearch] = useState(''); const [projectSearch, setProjectSearch] = useState(''); - const [branchSearch, setBranchSearch] = useState(''); + // 防抖搜索值 - 优化大量数据过滤性能 + const [debouncedGroupSearch, setDebouncedGroupSearch] = useState(''); + const [debouncedProjectSearch, setDebouncedProjectSearch] = useState(''); + const [debouncedBranchSearch, setDebouncedBranchSearch] = useState(''); - // 加载 Git 实例列表 - const loadInstances = async () => { - try { + // 仓库组搜索防抖 + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedGroupSearch(groupSearch); + }, 300); + return () => clearTimeout(timer); + }, [groupSearch]); - const data = await getGitInstances(); - if (data && Array.isArray(data)) { - setInstances(data); - if (data.length > 0 && !selectedInstanceId) { - setSelectedInstanceId(data[0].id); - } - } - } catch (error) { - toast({ - - variant: 'destructive', - title: '加载失败', - description: '加载 Git 实例列表失败', - }); - } - }; + // 项目搜索防抖 + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedProjectSearch(projectSearch); + }, 300); + return () => clearTimeout(timer); + }, [projectSearch]); + // 分支搜索防抖 + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedBranchSearch(branchSearch); + }, 300); + return () => clearTimeout(timer); + }, [branchSearch]); // 构建树形结构 - const buildTree = (groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => { + const buildTree = useCallback((groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => { // 使用 repoGroupId 作为 key,因为 parentId 对应的是 Git 系统中的 repoGroupId const map = new Map(); const roots: RepositoryGroupResponse[] = []; @@ -178,10 +184,10 @@ const GitManager: React.FC = () => { }); return roots; - }; + }, []); // 加载仓库组列表并构建树 - const loadGroupTree = async () => { + const loadGroupTree = useCallback(async () => { if (!selectedInstanceId) return; setLoading((prev) => ({ ...prev, groups: true })); @@ -202,10 +208,10 @@ const GitManager: React.FC = () => { } finally { setLoading((prev) => ({ ...prev, groups: false })); } - }; + }, [selectedInstanceId, buildTree, toast]); - // 加载项目列表 - const loadProjects = async (repoGroupId?: number) => { + // 加载项目列表(支持不传repoGroupId加载所有项目) + const loadProjects = useCallback(async (repoGroupId?: number) => { if (!selectedInstanceId) return; setLoading((prev) => ({ ...prev, projects: true })); @@ -225,10 +231,10 @@ const GitManager: React.FC = () => { } finally { setLoading((prev) => ({ ...prev, projects: false })); } - }; + }, [selectedInstanceId, toast]); - // 加载分支列表 - const loadBranches = async (repoProjectId: number) => { + // 加载分支列表(支持不传repoProjectId加载该组所有分支) + const loadBranches = useCallback(async (repoProjectId?: number) => { setLoading((prev) => ({ ...prev, branches: true })); try { const data = await getRepositoryBranches({ @@ -237,7 +243,6 @@ const GitManager: React.FC = () => { setBranches(data || []); } catch (error) { toast({ - variant: 'destructive', title: '加载失败', description: '加载分支列表失败', @@ -246,11 +251,10 @@ const GitManager: React.FC = () => { } finally { setLoading((prev) => ({ ...prev, branches: false })); } - }; + }, [toast]); // 同步所有数据(异步任务) - const handleSyncAll = async () => { - + const handleSyncAll = useCallback(async () => { if (!selectedInstanceId) return; setSyncing((prev) => ({ ...prev, all: true })); @@ -269,11 +273,10 @@ const GitManager: React.FC = () => { } finally { setSyncing((prev) => ({ ...prev, all: false })); } - }; + }, [selectedInstanceId, toast]); // 同步仓库组(异步任务) - const handleSyncGroups = async () => { - + const handleSyncGroups = useCallback(async () => { if (!selectedInstanceId) return; setSyncing((prev) => ({ ...prev, groups: true })); @@ -292,23 +295,27 @@ const GitManager: React.FC = () => { } finally { setSyncing((prev) => ({ ...prev, groups: false })); } - }; + }, [selectedInstanceId, toast]); - // 同步项目(异步任务) - const handleSyncProjects = async () => { - - if (!selectedInstanceId || !selectedGroup) return; + // 同步项目(异步任务,支持未选择组时同步所有项目) + const handleSyncProjects = useCallback(async () => { + if (!selectedInstanceId) return; setSyncing((prev) => ({ ...prev, projects: true })); try { // 传递 repoGroupId (Git系统中的ID) 和 externalSystemId await syncRepositoryProjects( selectedInstanceId, - selectedGroup.repoGroupId + selectedGroup?.repoGroupId ); + + const description = selectedGroup + ? `正在后台同步仓库组"${selectedGroup.name}"的项目,请稍后手动刷新查看结果` + : '正在后台同步所有项目,请稍后手动刷新查看结果'; + toast({ title: '同步任务已启动', - description: `正在后台同步仓库组"${selectedGroup.name}"的项目,请稍后手动刷新查看结果`, + description, }); } catch (error) { toast({ @@ -319,12 +326,11 @@ const GitManager: React.FC = () => { } finally { setSyncing((prev) => ({ ...prev, projects: false })); } - }; + }, [selectedInstanceId, selectedGroup, toast]); // 同步分支(异步任务) - const handleSyncBranches = async () => { - - if (!selectedInstanceId || !selectedGroup) return; + const handleSyncBranches = useCallback(async () => { + if (!selectedInstanceId) return; setSyncing((prev) => ({ ...prev, branches: true })); try { @@ -332,12 +338,15 @@ const GitManager: React.FC = () => { await syncRepositoryBranches( selectedInstanceId, selectedProject?.repoProjectId, - selectedGroup.repoGroupId + selectedGroup?.repoGroupId ); - const description = selectedProject - ? `正在后台同步项目"${selectedProject.name}"的分支,请稍后手动刷新查看结果` - : `正在后台同步组"${selectedGroup.name}"下所有项目的分支,请稍后手动刷新查看结果`; + let description = '正在后台同步所有分支,请稍后手动刷新查看结果'; + if (selectedProject) { + description = `正在后台同步项目"${selectedProject.name}"的分支,请稍后手动刷新查看结果`; + } else if (selectedGroup) { + description = `正在后台同步组"${selectedGroup.name}"下所有项目的分支,请稍后手动刷新查看结果`; + } toast({ title: '同步任务已启动', @@ -352,10 +361,10 @@ const GitManager: React.FC = () => { } finally { setSyncing((prev) => ({ ...prev, branches: false })); } - }; + }, [selectedInstanceId, selectedGroup, selectedProject, toast]); // 切换仓库组展开/折叠 - const toggleGroupExpand = (groupId: number) => { + const toggleGroupExpand = useCallback((groupId: number) => { setExpandedGroupIds((prev) => { const next = new Set(prev); if (next.has(groupId)) { @@ -365,37 +374,44 @@ const GitManager: React.FC = () => { } return next; }); - }; + }, []); + // 选中仓库组(支持反选取消) + const handleSelectGroup = useCallback((group: RepositoryGroupResponse) => { + // 如果点击的是已选中的组,则取消选择 + setSelectedGroup((prev) => { + const newGroup = prev?.id === group.id ? undefined : group; + setSelectedProject(undefined); + setBranches([]); + // 加载项目列表,如果取消选择则不传 repoGroupId(加载所有项目) + loadProjects(newGroup?.repoGroupId || newGroup?.id); + return newGroup; + }); + }, [loadProjects]); - // 选中仓库组 - const handleSelectGroup = (group: RepositoryGroupResponse) => { - setSelectedGroup(group); - setSelectedProject(undefined); - setBranches([]); - loadProjects(group.repoGroupId || group.id); - }; - - // 选中项目 - const handleSelectProject = (project: RepositoryProjectResponse) => { - setSelectedProject(project); - loadBranches(project.repoProjectId || project.id); - }; + // 选中项目(支持反选取消) + const handleSelectProject = useCallback((project: RepositoryProjectResponse) => { + // 如果点击的是已选中的项目,则取消选择 + setSelectedProject((prev) => { + const newProject = prev?.id === project.id ? undefined : project; + // 加载分支列表,如果取消选择则不传 repoProjectId(加载所有分支) + loadBranches(newProject?.repoProjectId || newProject?.id); + return newProject; + }); + }, [loadBranches]); // 格式化时间 - const formatTime = (time?: string) => { - + const formatTime = useCallback((time?: string) => { if (!time) return '-'; const diff = dayjs().diff(dayjs(time), 'day'); if (diff > 7) { return dayjs(time).format('YYYY-MM-DD HH:mm'); } return dayjs(time).fromNow(); - }; - + }, []); // 渲染可见性图标(带 Tooltip) - const renderVisibilityIcon = (visibility: string) => { + const renderVisibilityIcon = useCallback((visibility: string) => { const config = VISIBILITY_CONFIG[visibility.toLowerCase() as keyof typeof VISIBILITY_CONFIG] || VISIBILITY_CONFIG.public; @@ -412,17 +428,16 @@ const GitManager: React.FC = () => { ); - }; + }, []); - // 过滤仓库组树 - const filterGroupTree = (groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => { - if (!groupSearch) return groups; + // 过滤仓库组树(使用防抖搜索值) + const filterGroupTree = useCallback((groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => { + if (!debouncedGroupSearch) return groups; return groups.filter((group) => { const matchesSearch = - group.name.toLowerCase().includes(groupSearch.toLowerCase()) || - - (group.path && group.path.toLowerCase().includes(groupSearch.toLowerCase())); + group.name.toLowerCase().includes(debouncedGroupSearch.toLowerCase()) || + (group.path && group.path.toLowerCase().includes(debouncedGroupSearch.toLowerCase())); const filteredChildren = group.children ? filterGroupTree(group.children) @@ -433,7 +448,7 @@ const GitManager: React.FC = () => { ...group, children: group.children ? filterGroupTree(group.children) : [], })); - }; + }, [debouncedGroupSearch]); // 渲染仓库组树节点 const renderGroupTreeNode = ( @@ -480,7 +495,7 @@ const GitManager: React.FC = () => { {group.projectCount !== undefined && group.projectCount > 0 && ( {group.projectCount} - + )} {group.webUrl && ( @@ -516,33 +531,47 @@ const GitManager: React.FC = () => { ); }; - // 过滤项目列表 + // 过滤项目列表(使用防抖搜索值) const filteredProjects = useMemo(() => { - - if (!projectSearch) return projects; + if (!debouncedProjectSearch) return projects; return projects.filter( (project) => - project.name.toLowerCase().includes(projectSearch.toLowerCase()) || - - (project.path && project.path.toLowerCase().includes(projectSearch.toLowerCase())) + project.name.toLowerCase().includes(debouncedProjectSearch.toLowerCase()) || + (project.path && project.path.toLowerCase().includes(debouncedProjectSearch.toLowerCase())) ); - }, [projects, projectSearch]); + }, [projects, debouncedProjectSearch]); - // 过滤分支列表 + // 过滤分支列表(使用防抖搜索值) const filteredBranches = useMemo(() => { - if (!branchSearch) return branches; + if (!debouncedBranchSearch) return branches; return branches.filter((branch) => - branch.name.toLowerCase().includes(branchSearch.toLowerCase()) + branch.name.toLowerCase().includes(debouncedBranchSearch.toLowerCase()) ); - }, [branches, branchSearch]); + }, [branches, debouncedBranchSearch]); - // 初始化加载 + // 加载 Git 实例列表 useEffect(() => { - loadInstances(); - }, []); + const loadInstancesEffect = async () => { + try { + const data = await getGitInstances(); + if (data && Array.isArray(data)) { + setInstances(data); + if (data.length > 0 && !selectedInstanceId) { + setSelectedInstanceId(data[0].id); + } + } + } catch (error) { + toast({ + variant: 'destructive', + title: '加载失败', + description: '加载 Git 实例列表失败', + }); + } + }; + loadInstancesEffect(); + }, [selectedInstanceId, toast]); useEffect(() => { - if (selectedInstanceId) { loadGroupTree(); setSelectedGroup(undefined); @@ -550,9 +579,9 @@ const GitManager: React.FC = () => { setProjects([]); setBranches([]); } - }, [selectedInstanceId]); + }, [selectedInstanceId, loadGroupTree]); - const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, groupSearch]); + const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, filterGroupTree]); return ( @@ -610,19 +639,31 @@ const GitManager: React.FC = () => {
仓库组 +
+
+
@@ -634,7 +675,7 @@ const GitManager: React.FC = () => { className="pl-8 h-8" /> -
+
@@ -662,30 +703,40 @@ const GitManager: React.FC = () => {
- 项目 {selectedGroup && `(${filteredProjects.length})`} + 项目 ({filteredProjects.length}){selectedGroup && ` - ${selectedGroup.name}`} +
+
+
setProjectSearch(e.target.value)} - className="pl-8 h-8" />
@@ -695,18 +746,11 @@ const GitManager: React.FC = () => { {loading.projects ? (
-
- - ) : !selectedGroup ? ( -
- -

请先选择仓库组

- ) : filteredProjects.length === 0 ? ( - + ) : filteredProjects.length === 0 ? (
-

暂无项目

+

{selectedGroup ? '当前仓库组暂无项目' : '暂无项目'}

) : ( @@ -785,9 +829,9 @@ const GitManager: React.FC = () => { )} - ))} + ))} - )} + )} @@ -796,20 +840,33 @@ const GitManager: React.FC = () => {
- 分支 {selectedProject && `(${filteredBranches.length})`} + 分支 ({filteredBranches.length}){selectedProject && ` - ${selectedProject.name}`} - -
+
+ + +
+
{ {loading.branches ? (
-
- ) : !selectedProject ? ( -
- -

请先选择项目

-
+
) : filteredBranches.length === 0 ? (
-

暂无分支

-
- ) : ( +

{selectedProject ? '当前项目暂无分支' : '暂无分支'}

+ + ) : (
{filteredBranches.map((branch) => (
@@ -894,7 +946,7 @@ const GitManager: React.FC = () => { )}
)} -
+ {branch.webUrl && ( diff --git a/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx b/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx index fdb183e0..c5579831 100644 --- a/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx +++ b/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx @@ -1,18 +1,18 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { - Card, - CardHeader, - CardTitle, + Card, + CardHeader, + CardTitle, } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from '@/components/ui/select'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useToast } from '@/components/ui/use-toast'; @@ -37,6 +37,7 @@ import { AlertTriangle, Calendar, Activity, + Download, } from 'lucide-react'; import type { JenkinsInstance, @@ -53,6 +54,7 @@ import { syncJenkinsJobs, syncJenkinsBuilds, } from './service'; +import { useVirtualizer } from '@tanstack/react-virtual'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import 'dayjs/locale/zh-cn'; @@ -92,16 +94,16 @@ const JenkinsManager: React.FC = () => { // 加载状态 const [loading, setLoading] = useState({ - views: false, - jobs: false, - builds: false, + views: false, + jobs: false, + builds: false, }); // 同步状态 const [syncing, setSyncing] = useState({ - views: false, - jobs: false, - builds: false, + views: false, + jobs: false, + builds: false, }); // 搜索状态 @@ -109,28 +111,37 @@ const JenkinsManager: React.FC = () => { const [jobSearch, setJobSearch] = useState(''); const [buildSearch, setBuildSearch] = useState(''); - // 加载 Jenkins 实例列表 + // 防抖搜索值 - 优化大量数据过滤性能 + const [debouncedViewSearch, setDebouncedViewSearch] = useState(''); + const [debouncedJobSearch, setDebouncedJobSearch] = useState(''); + const [debouncedBuildSearch, setDebouncedBuildSearch] = useState(''); + + // 视图搜索防抖 useEffect(() => { - const loadInstances = async () => { - try { - const data = await getJenkinsInstances(); - setInstances(data); - if (data.length > 0 && !selectedInstanceId) { - setSelectedInstanceId(data[0].id); - } - } catch (error) { - toast({ - variant: 'destructive', - title: '加载失败', - description: '获取 Jenkins 实例列表失败', - }); - } - }; - loadInstances(); - }, []); + 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 loadViews = async () => { + const loadViews = useCallback(async () => { if (!selectedInstanceId) return; setLoading((prev) => ({ ...prev, views: true })); try { @@ -145,10 +156,10 @@ const JenkinsManager: React.FC = () => { } finally { setLoading((prev) => ({ ...prev, views: false })); } - }; + }, [selectedInstanceId, toast]); // 加载任务列表 - const loadJobs = async (viewId?: number) => { + const loadJobs = useCallback(async (viewId?: number) => { if (!selectedInstanceId) return; setLoading((prev) => ({ ...prev, jobs: true })); try { @@ -157,19 +168,19 @@ const JenkinsManager: React.FC = () => { viewId, }); setJobs(data || []); - } catch (error) { - toast({ - variant: 'destructive', + } catch (error) { + toast({ + variant: 'destructive', title: '加载失败', description: '获取任务列表失败', - }); - } finally { + }); + } finally { setLoading((prev) => ({ ...prev, jobs: false })); } - }; + }, [selectedInstanceId, toast]); // 加载构建列表 - const loadBuilds = async (jobId?: number) => { + const loadBuilds = useCallback(async (jobId?: number) => { if (!selectedInstanceId) return; setLoading((prev) => ({ ...prev, builds: true })); try { @@ -187,59 +198,52 @@ const JenkinsManager: React.FC = () => { } finally { setLoading((prev) => ({ ...prev, builds: false })); } - }; + }, [selectedInstanceId, toast]); - // 监听实例变化 + // 加载 Jenkins 实例列表 useEffect(() => { - if (selectedInstanceId) { - setSelectedView(undefined); - setSelectedJob(undefined); - setJobs([]); - setBuilds([]); - loadViews(); - } - }, [selectedInstanceId]); - - // 监听视图选择变化 - useEffect(() => { - if (selectedView) { - setSelectedJob(undefined); - setBuilds([]); - loadJobs(selectedView.id); - } - }, [selectedView]); - - // 监听任务选择变化 - useEffect(() => { - if (selectedJob) { - loadBuilds(selectedJob.id); - } - }, [selectedJob]); + const loadInstances = async () => { + try { + const data = await getJenkinsInstances(); + setInstances(data); + if (data.length > 0 && !selectedInstanceId) { + setSelectedInstanceId(data[0].id); + } + } catch (error) { + toast({ + variant: 'destructive', + title: '加载失败', + description: '获取 Jenkins 实例列表失败', + }); + } + }; + loadInstances(); + }, [selectedInstanceId, toast]); // 同步视图 - const handleSyncViews = async () => { + const handleSyncViews = useCallback(async () => { if (!selectedInstanceId) return; 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]); // 同步任务 - const handleSyncJobs = async () => { + const handleSyncJobs = useCallback(async () => { if (!selectedInstanceId) return; setSyncing((prev) => ({ ...prev, jobs: true })); try { @@ -247,28 +251,27 @@ const JenkinsManager: React.FC = () => { externalSystemId: selectedInstanceId, viewId: selectedView?.id, }); - toast({ - title: '同步成功', - description: '任务同步任务已启动', - }); + toast({ + title: '同步成功', + description: selectedView ? '视图任务同步任务已启动' : '所有任务同步任务已启动', + }); setTimeout(() => { - if (selectedView) { - loadJobs(selectedView.id); - } + // 无论是否选择视图,都重新加载任务列表 + 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]); // 同步构建 - const handleSyncBuilds = async () => { + const handleSyncBuilds = useCallback(async () => { if (!selectedInstanceId) return; setSyncing((prev) => ({ ...prev, builds: true })); try { @@ -276,64 +279,94 @@ const JenkinsManager: React.FC = () => { externalSystemId: selectedInstanceId, jobId: selectedJob?.id, }); - toast({ + toast({ title: '同步成功', - description: '构建同步任务已启动', - }); + description: selectedJob ? '任务构建同步任务已启动' : '所有构建同步任务已启动', + }); setTimeout(() => { - if (selectedJob) { - loadBuilds(selectedJob.id); - } + // 无论是否选择任务,都重新加载构建列表 + 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) { + setSelectedView(undefined); + setSelectedJob(undefined); + setJobs([]); + setBuilds([]); + loadViews(); } - }; + }, [selectedInstanceId, loadViews]); - // 过滤视图 + // 监听视图选择变化 + useEffect(() => { + if (selectedInstanceId) { + setSelectedJob(undefined); + setBuilds([]); + // 如果选择了视图,传递viewId;否则不传,加载所有任务 + loadJobs(selectedView?.id); + } + }, [selectedView, selectedInstanceId, loadJobs]); + + // 监听任务选择变化 + useEffect(() => { + if (selectedInstanceId) { + // 如果选择了任务,传递jobId;否则不传,加载所有构建 + loadBuilds(selectedJob?.id); + } + }, [selectedJob, selectedInstanceId, loadBuilds]); + + // 过滤视图(使用防抖搜索值) const filteredViews = useMemo(() => { + if (!debouncedViewSearch) return views; return views.filter((view) => - view.viewName.toLowerCase().includes(viewSearch.toLowerCase()) + view.viewName.toLowerCase().includes(debouncedViewSearch.toLowerCase()) ); - }, [views, viewSearch]); + }, [views, debouncedViewSearch]); - // 过滤任务 - const filteredJobs = useMemo(() => { + // 过滤任务(使用防抖搜索值) + const filteredJobs = useMemo(() => { + if (!debouncedJobSearch) return jobs; return jobs.filter((job) => - job.jobName.toLowerCase().includes(jobSearch.toLowerCase()) + job.jobName.toLowerCase().includes(debouncedJobSearch.toLowerCase()) ); - }, [jobs, jobSearch]); + }, [jobs, debouncedJobSearch]); - // 过滤构建 + // 过滤构建(使用防抖搜索值 - 特别重要,5000+条数据) const filteredBuilds = useMemo(() => { - if (!buildSearch) return builds; - const searchLower = buildSearch.toLowerCase(); + if (!debouncedBuildSearch) return builds; + const searchLower = debouncedBuildSearch.toLowerCase(); return builds.filter((build) => build.buildNumber.toString().includes(searchLower) || build.buildStatus.toLowerCase().includes(searchLower) ); - }, [builds, buildSearch]); + }, [builds, debouncedBuildSearch]); // 获取构建状态徽章 - const getBuildStatusBadge = (status: string) => { + const getBuildStatusBadge = useCallback((status: string) => { const config = BUILD_STATUS_CONFIG[status] || BUILD_STATUS_CONFIG.NOT_BUILT; const Icon = config.icon; - return ( + return ( {config.label} ); - }; + }, []); // 格式化时长 - const formatDuration = (duration?: number) => { + const formatDuration = useCallback((duration?: number) => { if (!duration) return '-'; const seconds = Math.floor(duration / 1000); if (seconds < 60) return `${seconds}秒`; @@ -341,12 +374,21 @@ const JenkinsManager: React.FC = () => { if (minutes < 60) return `${minutes}分钟`; const hours = Math.floor(minutes / 60); return `${hours}小时${minutes % 60}分钟`; - }; + }, []); - return ( + // 构建列表虚拟滚动 - 用于处理大量数据(5000+ 条) + const buildListRef = useRef(null); + const buildVirtualizer = useVirtualizer({ + count: filteredBuilds.length, + getScrollElement: () => buildListRef.current, + estimateSize: () => 120, // 预估每个构建卡片的高度 + overscan: 5, // 预渲染额外的项目数量 + }); + + return (
- {/* 页面标题 */} + {/* 页面标题 */}

Jenkins 管理

@@ -360,16 +402,16 @@ const JenkinsManager: React.FC = () => { > - - + + {instances.map((instance) => ( {instance.name} - - ))} - - -
+ + ))} + + +
{/* 三栏布局 */}
@@ -381,18 +423,31 @@ const JenkinsManager: React.FC = () => { 视图 ({filteredViews.length}) - -
+
+ + +
+
{ onChange={(e) => setViewSearch(e.target.value)} className="pl-8 h-8" /> -
-
+ +
{loading.views ? ( @@ -423,7 +478,7 @@ const JenkinsManager: React.FC = () => { ? 'bg-accent border-primary' : '' }`} - onClick={() => setSelectedView(view)} + onClick={() => setSelectedView(selectedView?.id === view.id ? undefined : view)} >
@@ -465,27 +520,40 @@ const JenkinsManager: React.FC = () => { )}
- + {/* 中栏:任务列表 */}
- 任务 {selectedView && `(${filteredJobs.length})`} + 任务 ({filteredJobs.length}){selectedView && ` - ${selectedView.viewName}`} - -
+
+ + +
+
{ onChange={(e) => setJobSearch(e.target.value)} className="pl-8 h-8" /> -
- + +
{loading.jobs ? (
- ) : !selectedView ? ( -
- -

请先选择视图

-
) : filteredJobs.length === 0 ? (
-

暂无任务

+

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

) : (
@@ -521,7 +584,7 @@ const JenkinsManager: React.FC = () => { ? 'bg-accent border-primary' : '' }`} - onClick={() => setSelectedJob(job)} + onClick={() => setSelectedJob(selectedJob?.id === job.id ? undefined : job)} >
{job.jobName} @@ -579,7 +642,7 @@ const JenkinsManager: React.FC = () => { )} -
+
{job.lastBuildTime && (
@@ -588,114 +651,143 @@ const JenkinsManager: React.FC = () => {
)}
- ))} - + ))} + )} - + {/* 右栏:构建列表 */}
- 构建 {selectedJob && `(${filteredBuilds.length})`} + 构建 ({filteredBuilds.length}){selectedJob && ` - ${selectedJob.jobName}`} - -
+
+ + +
+
- setBuildSearch(e.target.value)} className="pl-8 h-8" - /> -
-
-
+ /> +
+ +
{loading.builds ? (
- ) : !selectedJob ? ( -
- -

请先选择任务

-
) : filteredBuilds.length === 0 ? (
-

暂无构建

-
+

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

+
) : ( -
- {filteredBuilds.map((build) => ( -
-
-
- - #{build.buildNumber} - - {getBuildStatusBadge(build.buildStatus)} -
- {build.buildUrl && ( - - - - - - - -

在 Jenkins 中查看

-
-
+
+ {buildVirtualizer.getVirtualItems().map((virtualItem) => { + const build = filteredBuilds[virtualItem.index]; + return ( +
+
+
+ + #{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)} +
)}
- - {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/Deploy/JenkinsManager/List/service.ts b/frontend/src/pages/Deploy/JenkinsManager/List/service.ts index 412d9a26..99adff0b 100644 --- a/frontend/src/pages/Deploy/JenkinsManager/List/service.ts +++ b/frontend/src/pages/Deploy/JenkinsManager/List/service.ts @@ -28,7 +28,7 @@ export const syncJenkinsViews = (externalSystemId: number) => // 获取任务列表 export const getJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) => - request.get(`/api/v1/jenkins-job`, { params }); + request.get(`/api/v1/jenkins-job/list`, { params }); // 同步任务 export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) => @@ -38,7 +38,7 @@ export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: num // 获取构建列表 export const getJenkinsBuilds = (params: { externalSystemId: number; jobId?: number }) => - request.get(`/api/v1/jenkins-build`, { params }); + request.get(`/api/v1/jenkins-build/list`, { params }); // 同步构建(根据jobId) export const syncJenkinsBuilds = (params: { externalSystemId: number; jobId?: number }) =>