增加团队管理页面

This commit is contained in:
dengqichen 2025-10-30 10:18:23 +08:00
parent d5c4bb0c1f
commit e087b6abd8
5 changed files with 569 additions and 404 deletions

View File

@ -42,6 +42,7 @@
"@react-form-builder/designer": "^7.4.0", "@react-form-builder/designer": "^7.4.0",
"@react-form-builder/designer-bundle": "^7.4.0", "@react-form-builder/designer-bundle": "^7.4.0",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"@tanstack/react-virtual": "^3.13.12",
"@types/recharts": "^1.8.29", "@types/recharts": "^1.8.29",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@xyflow/react": "^12.8.6", "@xyflow/react": "^12.8.6",

View File

@ -104,6 +104,9 @@ importers:
'@reduxjs/toolkit': '@reduxjs/toolkit':
specifier: ^2.0.1 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) 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': '@types/recharts':
specifier: ^1.8.29 specifier: ^1.8.29
version: 1.8.29 version: 1.8.29
@ -1950,6 +1953,15 @@ packages:
react: '>=16.8.0' react: '>=16.8.0'
react-dom: '>=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': '@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@ -6167,6 +6179,14 @@ snapshots:
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(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': '@types/babel__core@7.20.5':
dependencies: dependencies:
'@babel/parser': 7.26.3 '@babel/parser': 7.26.3

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { import {
Card, Card,
CardHeader, CardHeader,
@ -43,6 +43,7 @@ import {
Calendar, Calendar,
User, User,
GitCommit, GitCommit,
Download,
} from 'lucide-react'; } from 'lucide-react';
import type { import type {
RepositoryGroupResponse, RepositoryGroupResponse,
@ -116,34 +117,39 @@ const GitManager: React.FC = () => {
// 搜索过滤 // 搜索过滤
const [groupSearch, setGroupSearch] = useState(''); const [groupSearch, setGroupSearch] = useState('');
const [projectSearch, setProjectSearch] = useState(''); const [projectSearch, setProjectSearch] = useState('');
const [branchSearch, setBranchSearch] = useState(''); const [branchSearch, setBranchSearch] = useState('');
// 防抖搜索值 - 优化大量数据过滤性能
const [debouncedGroupSearch, setDebouncedGroupSearch] = useState('');
const [debouncedProjectSearch, setDebouncedProjectSearch] = useState('');
const [debouncedBranchSearch, setDebouncedBranchSearch] = useState('');
// 加载 Git 实例列表 // 仓库组搜索防抖
const loadInstances = async () => { useEffect(() => {
try { const timer = setTimeout(() => {
setDebouncedGroupSearch(groupSearch);
}, 300);
return () => clearTimeout(timer);
}, [groupSearch]);
const data = await getGitInstances(); // 项目搜索防抖
if (data && Array.isArray(data)) { useEffect(() => {
setInstances(data); const timer = setTimeout(() => {
if (data.length > 0 && !selectedInstanceId) { setDebouncedProjectSearch(projectSearch);
setSelectedInstanceId(data[0].id); }, 300);
} return () => clearTimeout(timer);
} }, [projectSearch]);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '加载 Git 实例列表失败',
});
}
};
// 分支搜索防抖
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 // 使用 repoGroupId 作为 key因为 parentId 对应的是 Git 系统中的 repoGroupId
const map = new Map<number, RepositoryGroupResponse>(); const map = new Map<number, RepositoryGroupResponse>();
const roots: RepositoryGroupResponse[] = []; const roots: RepositoryGroupResponse[] = [];
@ -178,10 +184,10 @@ const GitManager: React.FC = () => {
}); });
return roots; return roots;
}; }, []);
// 加载仓库组列表并构建树 // 加载仓库组列表并构建树
const loadGroupTree = async () => { const loadGroupTree = useCallback(async () => {
if (!selectedInstanceId) return; if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, groups: true })); setLoading((prev) => ({ ...prev, groups: true }));
@ -202,10 +208,10 @@ const GitManager: React.FC = () => {
} finally { } finally {
setLoading((prev) => ({ ...prev, groups: false })); setLoading((prev) => ({ ...prev, groups: false }));
} }
}; }, [selectedInstanceId, buildTree, toast]);
// 加载项目列表 // 加载项目列表支持不传repoGroupId加载所有项目
const loadProjects = async (repoGroupId?: number) => { const loadProjects = useCallback(async (repoGroupId?: number) => {
if (!selectedInstanceId) return; if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, projects: true })); setLoading((prev) => ({ ...prev, projects: true }));
@ -225,10 +231,10 @@ const GitManager: React.FC = () => {
} finally { } finally {
setLoading((prev) => ({ ...prev, projects: false })); setLoading((prev) => ({ ...prev, projects: false }));
} }
}; }, [selectedInstanceId, toast]);
// 加载分支列表 // 加载分支列表支持不传repoProjectId加载该组所有分支
const loadBranches = async (repoProjectId: number) => { const loadBranches = useCallback(async (repoProjectId?: number) => {
setLoading((prev) => ({ ...prev, branches: true })); setLoading((prev) => ({ ...prev, branches: true }));
try { try {
const data = await getRepositoryBranches({ const data = await getRepositoryBranches({
@ -237,7 +243,6 @@ const GitManager: React.FC = () => {
setBranches(data || []); setBranches(data || []);
} catch (error) { } catch (error) {
toast({ toast({
variant: 'destructive', variant: 'destructive',
title: '加载失败', title: '加载失败',
description: '加载分支列表失败', description: '加载分支列表失败',
@ -246,11 +251,10 @@ const GitManager: React.FC = () => {
} finally { } finally {
setLoading((prev) => ({ ...prev, branches: false })); setLoading((prev) => ({ ...prev, branches: false }));
} }
}; }, [toast]);
// 同步所有数据(异步任务) // 同步所有数据(异步任务)
const handleSyncAll = async () => { const handleSyncAll = useCallback(async () => {
if (!selectedInstanceId) return; if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, all: true })); setSyncing((prev) => ({ ...prev, all: true }));
@ -269,11 +273,10 @@ const GitManager: React.FC = () => {
} finally { } finally {
setSyncing((prev) => ({ ...prev, all: false })); setSyncing((prev) => ({ ...prev, all: false }));
} }
}; }, [selectedInstanceId, toast]);
// 同步仓库组(异步任务) // 同步仓库组(异步任务)
const handleSyncGroups = async () => { const handleSyncGroups = useCallback(async () => {
if (!selectedInstanceId) return; if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, groups: true })); setSyncing((prev) => ({ ...prev, groups: true }));
@ -292,23 +295,27 @@ const GitManager: React.FC = () => {
} finally { } finally {
setSyncing((prev) => ({ ...prev, groups: false })); setSyncing((prev) => ({ ...prev, groups: false }));
} }
}; }, [selectedInstanceId, toast]);
// 同步项目(异步任务) // 同步项目(异步任务,支持未选择组时同步所有项目)
const handleSyncProjects = async () => { const handleSyncProjects = useCallback(async () => {
if (!selectedInstanceId) return;
if (!selectedInstanceId || !selectedGroup) return;
setSyncing((prev) => ({ ...prev, projects: true })); setSyncing((prev) => ({ ...prev, projects: true }));
try { try {
// 传递 repoGroupId (Git系统中的ID) 和 externalSystemId // 传递 repoGroupId (Git系统中的ID) 和 externalSystemId
await syncRepositoryProjects( await syncRepositoryProjects(
selectedInstanceId, selectedInstanceId,
selectedGroup.repoGroupId selectedGroup?.repoGroupId
); );
const description = selectedGroup
? `正在后台同步仓库组"${selectedGroup.name}"的项目,请稍后手动刷新查看结果`
: '正在后台同步所有项目,请稍后手动刷新查看结果';
toast({ toast({
title: '同步任务已启动', title: '同步任务已启动',
description: `正在后台同步仓库组"${selectedGroup.name}"的项目,请稍后手动刷新查看结果`, description,
}); });
} catch (error) { } catch (error) {
toast({ toast({
@ -319,12 +326,11 @@ const GitManager: React.FC = () => {
} finally { } finally {
setSyncing((prev) => ({ ...prev, projects: false })); setSyncing((prev) => ({ ...prev, projects: false }));
} }
}; }, [selectedInstanceId, selectedGroup, toast]);
// 同步分支(异步任务) // 同步分支(异步任务)
const handleSyncBranches = async () => { const handleSyncBranches = useCallback(async () => {
if (!selectedInstanceId) return;
if (!selectedInstanceId || !selectedGroup) return;
setSyncing((prev) => ({ ...prev, branches: true })); setSyncing((prev) => ({ ...prev, branches: true }));
try { try {
@ -332,12 +338,15 @@ const GitManager: React.FC = () => {
await syncRepositoryBranches( await syncRepositoryBranches(
selectedInstanceId, selectedInstanceId,
selectedProject?.repoProjectId, selectedProject?.repoProjectId,
selectedGroup.repoGroupId selectedGroup?.repoGroupId
); );
const description = selectedProject let description = '正在后台同步所有分支,请稍后手动刷新查看结果';
? `正在后台同步项目"${selectedProject.name}"的分支,请稍后手动刷新查看结果` if (selectedProject) {
: `正在后台同步组"${selectedGroup.name}"下所有项目的分支,请稍后手动刷新查看结果`; description = `正在后台同步项目"${selectedProject.name}"的分支,请稍后手动刷新查看结果`;
} else if (selectedGroup) {
description = `正在后台同步组"${selectedGroup.name}"下所有项目的分支,请稍后手动刷新查看结果`;
}
toast({ toast({
title: '同步任务已启动', title: '同步任务已启动',
@ -352,10 +361,10 @@ const GitManager: React.FC = () => {
} finally { } finally {
setSyncing((prev) => ({ ...prev, branches: false })); setSyncing((prev) => ({ ...prev, branches: false }));
} }
}; }, [selectedInstanceId, selectedGroup, selectedProject, toast]);
// 切换仓库组展开/折叠 // 切换仓库组展开/折叠
const toggleGroupExpand = (groupId: number) => { const toggleGroupExpand = useCallback((groupId: number) => {
setExpandedGroupIds((prev) => { setExpandedGroupIds((prev) => {
const next = new Set(prev); const next = new Set(prev);
if (next.has(groupId)) { if (next.has(groupId)) {
@ -365,37 +374,44 @@ const GitManager: React.FC = () => {
} }
return next; return next;
}); });
}; }, []);
// 选中仓库组(支持反选取消)
// 选中仓库组 const handleSelectGroup = useCallback((group: RepositoryGroupResponse) => {
const handleSelectGroup = (group: RepositoryGroupResponse) => { // 如果点击的是已选中的组,则取消选择
setSelectedGroup(group); setSelectedGroup((prev) => {
const newGroup = prev?.id === group.id ? undefined : group;
setSelectedProject(undefined); setSelectedProject(undefined);
setBranches([]); setBranches([]);
loadProjects(group.repoGroupId || group.id); // 加载项目列表,如果取消选择则不传 repoGroupId加载所有项目
}; loadProjects(newGroup?.repoGroupId || newGroup?.id);
return newGroup;
});
}, [loadProjects]);
// 选中项目 // 选中项目(支持反选取消)
const handleSelectProject = (project: RepositoryProjectResponse) => { const handleSelectProject = useCallback((project: RepositoryProjectResponse) => {
setSelectedProject(project); // 如果点击的是已选中的项目,则取消选择
loadBranches(project.repoProjectId || project.id); 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 '-'; if (!time) return '-';
const diff = dayjs().diff(dayjs(time), 'day'); const diff = dayjs().diff(dayjs(time), 'day');
if (diff > 7) { if (diff > 7) {
return dayjs(time).format('YYYY-MM-DD HH:mm'); return dayjs(time).format('YYYY-MM-DD HH:mm');
} }
return dayjs(time).fromNow(); return dayjs(time).fromNow();
}; }, []);
// 渲染可见性图标(带 Tooltip // 渲染可见性图标(带 Tooltip
const renderVisibilityIcon = (visibility: string) => { const renderVisibilityIcon = useCallback((visibility: string) => {
const config = const config =
VISIBILITY_CONFIG[visibility.toLowerCase() as keyof typeof VISIBILITY_CONFIG] || VISIBILITY_CONFIG[visibility.toLowerCase() as keyof typeof VISIBILITY_CONFIG] ||
VISIBILITY_CONFIG.public; VISIBILITY_CONFIG.public;
@ -412,17 +428,16 @@ const GitManager: React.FC = () => {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
); );
}; }, []);
// 过滤仓库组树 // 过滤仓库组树(使用防抖搜索值)
const filterGroupTree = (groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => { const filterGroupTree = useCallback((groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => {
if (!groupSearch) return groups; if (!debouncedGroupSearch) return groups;
return groups.filter((group) => { return groups.filter((group) => {
const matchesSearch = const matchesSearch =
group.name.toLowerCase().includes(groupSearch.toLowerCase()) || group.name.toLowerCase().includes(debouncedGroupSearch.toLowerCase()) ||
(group.path && group.path.toLowerCase().includes(debouncedGroupSearch.toLowerCase()));
(group.path && group.path.toLowerCase().includes(groupSearch.toLowerCase()));
const filteredChildren = group.children const filteredChildren = group.children
? filterGroupTree(group.children) ? filterGroupTree(group.children)
@ -433,7 +448,7 @@ const GitManager: React.FC = () => {
...group, ...group,
children: group.children ? filterGroupTree(group.children) : [], children: group.children ? filterGroupTree(group.children) : [],
})); }));
}; }, [debouncedGroupSearch]);
// 渲染仓库组树节点 // 渲染仓库组树节点
const renderGroupTreeNode = ( const renderGroupTreeNode = (
@ -516,33 +531,47 @@ const GitManager: React.FC = () => {
); );
}; };
// 过滤项目列表 // 过滤项目列表(使用防抖搜索值)
const filteredProjects = useMemo(() => { const filteredProjects = useMemo(() => {
if (!debouncedProjectSearch) return projects;
if (!projectSearch) return projects;
return projects.filter( return projects.filter(
(project) => (project) =>
project.name.toLowerCase().includes(projectSearch.toLowerCase()) || project.name.toLowerCase().includes(debouncedProjectSearch.toLowerCase()) ||
(project.path && project.path.toLowerCase().includes(debouncedProjectSearch.toLowerCase()))
(project.path && project.path.toLowerCase().includes(projectSearch.toLowerCase()))
); );
}, [projects, projectSearch]); }, [projects, debouncedProjectSearch]);
// 过滤分支列表 // 过滤分支列表(使用防抖搜索值)
const filteredBranches = useMemo(() => { const filteredBranches = useMemo(() => {
if (!branchSearch) return branches; if (!debouncedBranchSearch) return branches;
return branches.filter((branch) => return branches.filter((branch) =>
branch.name.toLowerCase().includes(branchSearch.toLowerCase()) branch.name.toLowerCase().includes(debouncedBranchSearch.toLowerCase())
); );
}, [branches, branchSearch]); }, [branches, debouncedBranchSearch]);
// 初始化加载 // 加载 Git 实例列表
useEffect(() => { 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(() => { useEffect(() => {
if (selectedInstanceId) { if (selectedInstanceId) {
loadGroupTree(); loadGroupTree();
setSelectedGroup(undefined); setSelectedGroup(undefined);
@ -550,9 +579,9 @@ const GitManager: React.FC = () => {
setProjects([]); setProjects([]);
setBranches([]); setBranches([]);
} }
}, [selectedInstanceId]); }, [selectedInstanceId, loadGroupTree]);
const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, groupSearch]); const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, filterGroupTree]);
return ( return (
@ -610,19 +639,31 @@ const GitManager: React.FC = () => {
<CardHeader className="border-b flex-shrink-0"> <CardHeader className="border-b flex-shrink-0">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={loadGroupTree}
disabled={loading.groups || !selectedInstanceId}
>
<RefreshCw
className={`h-4 w-4 ${loading.groups ? 'animate-spin' : ''}`}
/>
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-7 w-7"
onClick={handleSyncGroups} onClick={handleSyncGroups}
disabled={syncing.groups || !selectedInstanceId} disabled={syncing.groups || !selectedInstanceId}
> >
<RefreshCw <Download
className={`h-4 w-4 ${syncing.groups ? 'animate-spin' : ''}`} className={`h-4 w-4 ${syncing.groups ? 'animate-bounce' : ''}`}
/> />
</Button> </Button>
</div> </div>
</div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@ -662,30 +703,40 @@ const GitManager: React.FC = () => {
<CardHeader className="border-b flex-shrink-0"> <CardHeader className="border-b flex-shrink-0">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base"> <CardTitle className="text-base">
{selectedGroup && `(${filteredProjects.length})`} ({filteredProjects.length}){selectedGroup && ` - ${selectedGroup.name}`}
</CardTitle> </CardTitle>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => loadProjects(selectedGroup?.repoGroupId || selectedGroup?.id)}
disabled={loading.projects || !selectedInstanceId}
>
<RefreshCw
className={`h-4 w-4 ${loading.projects ? 'animate-spin' : ''}`}
/>
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-7 w-7"
onClick={handleSyncProjects} onClick={handleSyncProjects}
disabled={syncing.projects || !selectedInstanceId} disabled={syncing.projects || !selectedInstanceId}
> >
<RefreshCw <Download
className={`h-4 w-4 ${syncing.projects ? 'animate-spin' : ''}`} className={`h-4 w-4 ${syncing.projects ? 'animate-bounce' : ''}`}
/> />
</Button> </Button>
</div> </div>
</div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
placeholder="搜索项目..." placeholder="搜索项目..."
value={projectSearch} value={projectSearch}
onChange={(e) => setProjectSearch(e.target.value)} onChange={(e) => setProjectSearch(e.target.value)}
className="pl-8 h-8" className="pl-8 h-8"
/> />
</div> </div>
@ -696,17 +747,10 @@ const GitManager: React.FC = () => {
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
) : !selectedGroup ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Code2 className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p>
</div>
) : filteredProjects.length === 0 ? ( ) : filteredProjects.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground"> <div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Code2 className="h-12 w-12 mb-2 opacity-20" /> <Code2 className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p> <p className="text-sm">{selectedGroup ? '当前仓库组暂无项目' : '暂无项目'}</p>
</div> </div>
) : ( ) : (
@ -796,20 +840,33 @@ const GitManager: React.FC = () => {
<CardHeader className="border-b flex-shrink-0"> <CardHeader className="border-b flex-shrink-0">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base"> <CardTitle className="text-base">
{selectedProject && `(${filteredBranches.length})`} ({filteredBranches.length}){selectedProject && ` - ${selectedProject.name}`}
</CardTitle> </CardTitle>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => loadBranches(selectedProject?.repoProjectId || selectedProject?.id)}
disabled={loading.branches || !selectedInstanceId}
>
<RefreshCw
className={`h-4 w-4 ${loading.branches ? 'animate-spin' : ''}`}
/>
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-7 w-7"
onClick={handleSyncBranches} onClick={handleSyncBranches}
disabled={syncing.branches || !selectedGroup} disabled={syncing.branches || !selectedInstanceId}
> >
<RefreshCw <Download
className={`h-4 w-4 ${syncing.branches ? 'animate-spin' : ''}`} className={`h-4 w-4 ${syncing.branches ? 'animate-bounce' : ''}`}
/> />
</Button> </Button>
</div> </div>
</div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
@ -825,15 +882,10 @@ const GitManager: React.FC = () => {
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
) : !selectedProject ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<GitBranch className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p>
</div>
) : filteredBranches.length === 0 ? ( ) : filteredBranches.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground"> <div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<GitBranch className="h-12 w-12 mb-2 opacity-20" /> <GitBranch className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p> <p className="text-sm">{selectedProject ? '当前项目暂无分支' : '暂无分支'}</p>
</div> </div>
) : ( ) : (
<div className="divide-y"> <div className="divide-y">

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { import {
Card, Card,
CardHeader, CardHeader,
@ -37,6 +37,7 @@ import {
AlertTriangle, AlertTriangle,
Calendar, Calendar,
Activity, Activity,
Download,
} from 'lucide-react'; } from 'lucide-react';
import type { import type {
JenkinsInstance, JenkinsInstance,
@ -53,6 +54,7 @@ import {
syncJenkinsJobs, syncJenkinsJobs,
syncJenkinsBuilds, syncJenkinsBuilds,
} from './service'; } from './service';
import { useVirtualizer } from '@tanstack/react-virtual';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/zh-cn'; import 'dayjs/locale/zh-cn';
@ -109,6 +111,95 @@ const JenkinsManager: React.FC = () => {
const [jobSearch, setJobSearch] = useState(''); const [jobSearch, setJobSearch] = useState('');
const [buildSearch, setBuildSearch] = useState(''); const [buildSearch, setBuildSearch] = useState('');
// 防抖搜索值 - 优化大量数据过滤性能
const [debouncedViewSearch, setDebouncedViewSearch] = useState('');
const [debouncedJobSearch, setDebouncedJobSearch] = useState('');
const [debouncedBuildSearch, setDebouncedBuildSearch] = useState('');
// 视图搜索防抖
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 loadViews = useCallback(async () => {
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, views: true }));
try {
const data = await getJenkinsViews(selectedInstanceId);
setViews(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '获取视图列表失败',
});
} finally {
setLoading((prev) => ({ ...prev, views: false }));
}
}, [selectedInstanceId, toast]);
// 加载任务列表
const loadJobs = useCallback(async (viewId?: number) => {
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, jobs: true }));
try {
const data = await getJenkinsJobs({
externalSystemId: selectedInstanceId,
viewId,
});
setJobs(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '获取任务列表失败',
});
} finally {
setLoading((prev) => ({ ...prev, jobs: false }));
}
}, [selectedInstanceId, toast]);
// 加载构建列表
const loadBuilds = useCallback(async (jobId?: number) => {
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, builds: true }));
try {
const data = await getJenkinsBuilds({
externalSystemId: selectedInstanceId,
jobId,
});
setBuilds(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '获取构建列表失败',
});
} finally {
setLoading((prev) => ({ ...prev, builds: false }));
}
}, [selectedInstanceId, toast]);
// 加载 Jenkins 实例列表 // 加载 Jenkins 实例列表
useEffect(() => { useEffect(() => {
const loadInstances = async () => { const loadInstances = async () => {
@ -127,97 +218,10 @@ const JenkinsManager: React.FC = () => {
} }
}; };
loadInstances(); loadInstances();
}, []); }, [selectedInstanceId, toast]);
// 加载视图列表
const loadViews = async () => {
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, views: true }));
try {
const data = await getJenkinsViews(selectedInstanceId);
setViews(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '获取视图列表失败',
});
} finally {
setLoading((prev) => ({ ...prev, views: false }));
}
};
// 加载任务列表
const loadJobs = async (viewId?: number) => {
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, jobs: true }));
try {
const data = await getJenkinsJobs({
externalSystemId: selectedInstanceId,
viewId,
});
setJobs(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '获取任务列表失败',
});
} finally {
setLoading((prev) => ({ ...prev, jobs: false }));
}
};
// 加载构建列表
const loadBuilds = async (jobId?: number) => {
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, builds: true }));
try {
const data = await getJenkinsBuilds({
externalSystemId: selectedInstanceId,
jobId,
});
setBuilds(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '获取构建列表失败',
});
} finally {
setLoading((prev) => ({ ...prev, builds: false }));
}
};
// 监听实例变化
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 handleSyncViews = async () => { const handleSyncViews = useCallback(async () => {
if (!selectedInstanceId) return; if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, views: true })); setSyncing((prev) => ({ ...prev, views: true }));
try { try {
@ -236,10 +240,10 @@ const JenkinsManager: React.FC = () => {
} finally { } finally {
setSyncing((prev) => ({ ...prev, views: false })); setSyncing((prev) => ({ ...prev, views: false }));
} }
}; }, [selectedInstanceId, toast, loadViews]);
// 同步任务 // 同步任务
const handleSyncJobs = async () => { const handleSyncJobs = useCallback(async () => {
if (!selectedInstanceId) return; if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, jobs: true })); setSyncing((prev) => ({ ...prev, jobs: true }));
try { try {
@ -249,12 +253,11 @@ const JenkinsManager: React.FC = () => {
}); });
toast({ toast({
title: '同步成功', title: '同步成功',
description: '任务同步任务已启动', description: selectedView ? '视图任务同步任务已启动' : '所有任务同步任务已启动',
}); });
setTimeout(() => { setTimeout(() => {
if (selectedView) { // 无论是否选择视图,都重新加载任务列表
loadJobs(selectedView.id); loadJobs(selectedView?.id);
}
}, 2000); }, 2000);
} catch (error) { } catch (error) {
toast({ toast({
@ -265,10 +268,10 @@ const JenkinsManager: React.FC = () => {
} finally { } finally {
setSyncing((prev) => ({ ...prev, jobs: false })); setSyncing((prev) => ({ ...prev, jobs: false }));
} }
}; }, [selectedInstanceId, selectedView, toast, loadJobs]);
// 同步构建 // 同步构建
const handleSyncBuilds = async () => { const handleSyncBuilds = useCallback(async () => {
if (!selectedInstanceId) return; if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, builds: true })); setSyncing((prev) => ({ ...prev, builds: true }));
try { try {
@ -278,12 +281,11 @@ const JenkinsManager: React.FC = () => {
}); });
toast({ toast({
title: '同步成功', title: '同步成功',
description: '构建同步任务已启动', description: selectedJob ? '任务构建同步任务已启动' : '所有构建同步任务已启动',
}); });
setTimeout(() => { setTimeout(() => {
if (selectedJob) { // 无论是否选择任务,都重新加载构建列表
loadBuilds(selectedJob.id); loadBuilds(selectedJob?.id);
}
}, 2000); }, 2000);
} catch (error) { } catch (error) {
toast({ toast({
@ -294,34 +296,65 @@ const JenkinsManager: React.FC = () => {
} finally { } finally {
setSyncing((prev) => ({ ...prev, builds: false })); 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(() => { const filteredViews = useMemo(() => {
if (!debouncedViewSearch) return views;
return views.filter((view) => 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) => return jobs.filter((job) =>
job.jobName.toLowerCase().includes(jobSearch.toLowerCase()) job.jobName.toLowerCase().includes(debouncedJobSearch.toLowerCase())
); );
}, [jobs, jobSearch]); }, [jobs, debouncedJobSearch]);
// 过滤构建 // 过滤构建(使用防抖搜索值 - 特别重要5000+条数据)
const filteredBuilds = useMemo(() => { const filteredBuilds = useMemo(() => {
if (!buildSearch) return builds; if (!debouncedBuildSearch) return builds;
const searchLower = buildSearch.toLowerCase(); const searchLower = debouncedBuildSearch.toLowerCase();
return builds.filter((build) => return builds.filter((build) =>
build.buildNumber.toString().includes(searchLower) || build.buildNumber.toString().includes(searchLower) ||
build.buildStatus.toLowerCase().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 config = BUILD_STATUS_CONFIG[status] || BUILD_STATUS_CONFIG.NOT_BUILT;
const Icon = config.icon; const Icon = config.icon;
return ( return (
@ -330,10 +363,10 @@ const JenkinsManager: React.FC = () => {
{config.label} {config.label}
</Badge> </Badge>
); );
}; }, []);
// 格式化时长 // 格式化时长
const formatDuration = (duration?: number) => { const formatDuration = useCallback((duration?: number) => {
if (!duration) return '-'; if (!duration) return '-';
const seconds = Math.floor(duration / 1000); const seconds = Math.floor(duration / 1000);
if (seconds < 60) return `${seconds}`; if (seconds < 60) return `${seconds}`;
@ -341,7 +374,16 @@ const JenkinsManager: React.FC = () => {
if (minutes < 60) return `${minutes}分钟`; if (minutes < 60) return `${minutes}分钟`;
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
return `${hours}小时${minutes % 60}分钟`; return `${hours}小时${minutes % 60}分钟`;
}; }, []);
// 构建列表虚拟滚动 - 用于处理大量数据5000+ 条)
const buildListRef = useRef<HTMLDivElement>(null);
const buildVirtualizer = useVirtualizer({
count: filteredBuilds.length,
getScrollElement: () => buildListRef.current,
estimateSize: () => 120, // 预估每个构建卡片的高度
overscan: 5, // 预渲染额外的项目数量
});
return ( return (
<TooltipProvider> <TooltipProvider>
@ -381,6 +423,18 @@ const JenkinsManager: React.FC = () => {
<Layers className="h-4 w-4" /> <Layers className="h-4 w-4" />
({filteredViews.length}) ({filteredViews.length})
</CardTitle> </CardTitle>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={loadViews}
disabled={loading.views || !selectedInstanceId}
>
<RefreshCw
className={`h-4 w-4 ${loading.views ? 'animate-spin' : ''}`}
/>
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -388,11 +442,12 @@ const JenkinsManager: React.FC = () => {
onClick={handleSyncViews} onClick={handleSyncViews}
disabled={syncing.views || !selectedInstanceId} disabled={syncing.views || !selectedInstanceId}
> >
<RefreshCw <Download
className={`h-4 w-4 ${syncing.views ? 'animate-spin' : ''}`} className={`h-4 w-4 ${syncing.views ? 'animate-bounce' : ''}`}
/> />
</Button> </Button>
</div> </div>
</div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
@ -423,7 +478,7 @@ const JenkinsManager: React.FC = () => {
? 'bg-accent border-primary' ? 'bg-accent border-primary'
: '' : ''
}`} }`}
onClick={() => setSelectedView(view)} onClick={() => setSelectedView(selectedView?.id === view.id ? undefined : view)}
> >
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="flex-1 truncate font-medium"> <span className="flex-1 truncate font-medium">
@ -472,8 +527,20 @@ const JenkinsManager: React.FC = () => {
<CardHeader className="border-b flex-shrink-0"> <CardHeader className="border-b flex-shrink-0">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base"> <CardTitle className="text-base">
{selectedView && `(${filteredJobs.length})`} ({filteredJobs.length}){selectedView && ` - ${selectedView.viewName}`}
</CardTitle> </CardTitle>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => loadJobs(selectedView?.id)}
disabled={loading.jobs || !selectedInstanceId}
>
<RefreshCw
className={`h-4 w-4 ${loading.jobs ? 'animate-spin' : ''}`}
/>
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -481,11 +548,12 @@ const JenkinsManager: React.FC = () => {
onClick={handleSyncJobs} onClick={handleSyncJobs}
disabled={syncing.jobs || !selectedInstanceId} disabled={syncing.jobs || !selectedInstanceId}
> >
<RefreshCw <Download
className={`h-4 w-4 ${syncing.jobs ? 'animate-spin' : ''}`} className={`h-4 w-4 ${syncing.jobs ? 'animate-bounce' : ''}`}
/> />
</Button> </Button>
</div> </div>
</div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
@ -501,15 +569,10 @@ const JenkinsManager: React.FC = () => {
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
) : !selectedView ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Box className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p>
</div>
) : filteredJobs.length === 0 ? ( ) : filteredJobs.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground"> <div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Box className="h-12 w-12 mb-2 opacity-20" /> <Box className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p> <p className="text-sm">{selectedView ? '当前视图暂无任务' : '暂无任务'}</p>
</div> </div>
) : ( ) : (
<div className="p-2 space-y-2"> <div className="p-2 space-y-2">
@ -521,7 +584,7 @@ const JenkinsManager: React.FC = () => {
? 'bg-accent border-primary' ? 'bg-accent border-primary'
: '' : ''
}`} }`}
onClick={() => setSelectedJob(job)} onClick={() => setSelectedJob(selectedJob?.id === job.id ? undefined : job)}
> >
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="flex-1 truncate font-medium">{job.jobName}</span> <span className="flex-1 truncate font-medium">{job.jobName}</span>
@ -599,8 +662,20 @@ const JenkinsManager: React.FC = () => {
<CardHeader className="border-b flex-shrink-0"> <CardHeader className="border-b flex-shrink-0">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base"> <CardTitle className="text-base">
{selectedJob && `(${filteredBuilds.length})`} ({filteredBuilds.length}){selectedJob && ` - ${selectedJob.jobName}`}
</CardTitle> </CardTitle>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => loadBuilds(selectedJob?.id)}
disabled={loading.builds || !selectedInstanceId}
>
<RefreshCw
className={`h-4 w-4 ${loading.builds ? 'animate-spin' : ''}`}
/>
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -608,10 +683,11 @@ const JenkinsManager: React.FC = () => {
onClick={handleSyncBuilds} onClick={handleSyncBuilds}
disabled={syncing.builds || !selectedInstanceId} disabled={syncing.builds || !selectedInstanceId}
> >
<RefreshCw <Download
className={`h-4 w-4 ${syncing.builds ? 'animate-spin' : ''}`} className={`h-4 w-4 ${syncing.builds ? 'animate-bounce' : ''}`}
/> />
</Button> </Button>
</div>
</div> </div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@ -623,27 +699,42 @@ const JenkinsManager: React.FC = () => {
/> />
</div> </div>
</CardHeader> </CardHeader>
<div className="flex-1 overflow-auto"> <div
ref={buildListRef}
className="flex-1 overflow-auto"
>
{loading.builds ? ( {loading.builds ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
) : !selectedJob ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<GitBranch className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p>
</div>
) : filteredBuilds.length === 0 ? ( ) : filteredBuilds.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground"> <div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<GitBranch className="h-12 w-12 mb-2 opacity-20" /> <GitBranch className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p> <p className="text-sm">{selectedJob ? '当前任务暂无构建' : '暂无构建'}</p>
</div> </div>
) : ( ) : (
<div className="divide-y"> <div
{filteredBuilds.map((build) => ( style={{
height: `${buildVirtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{buildVirtualizer.getVirtualItems().map((virtualItem) => {
const build = filteredBuilds[virtualItem.index];
return (
<div <div
key={build.id} key={build.id}
className="group p-3 hover:bg-accent transition-colors" data-index={virtualItem.index}
ref={buildVirtualizer.measureElement}
className="group p-3 hover:bg-accent transition-colors border-b"
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
> >
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -687,7 +778,8 @@ const JenkinsManager: React.FC = () => {
</div> </div>
)} )}
</div> </div>
))} );
})}
</div> </div>
)} )}
</div> </div>

View File

@ -28,7 +28,7 @@ export const syncJenkinsViews = (externalSystemId: number) =>
// 获取任务列表 // 获取任务列表
export const getJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) => export const getJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) =>
request.get<JenkinsJobDTO[]>(`/api/v1/jenkins-job`, { params }); request.get<JenkinsJobDTO[]>(`/api/v1/jenkins-job/list`, { params });
// 同步任务 // 同步任务
export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) => 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 }) => export const getJenkinsBuilds = (params: { externalSystemId: number; jobId?: number }) =>
request.get<JenkinsBuildDTO[]>(`/api/v1/jenkins-build`, { params }); request.get<JenkinsBuildDTO[]>(`/api/v1/jenkins-build/list`, { params });
// 同步构建根据jobId // 同步构建根据jobId
export const syncJenkinsBuilds = (params: { externalSystemId: number; jobId?: number }) => export const syncJenkinsBuilds = (params: { externalSystemId: number; jobId?: number }) =>