增加团队管理页面

This commit is contained in:
dengqichen 2025-10-30 13:38:18 +08:00
parent 7fa9b08e3d
commit c24f825696
7 changed files with 730 additions and 436 deletions

View File

@ -60,6 +60,7 @@
"react-diff-viewer-continued": "^3.4.0", "react-diff-viewer-continued": "^3.4.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-infinite-scroll-component": "^6.1.0",
"react-redux": "^9.0.4", "react-redux": "^9.0.4",
"react-router-dom": "^6.21.0", "react-router-dom": "^6.21.0",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
@ -74,6 +75,7 @@
"@types/node": "^20.17.10", "@types/node": "^20.17.10",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"@types/react-infinite-scroll-component": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0", "@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",

View File

@ -158,6 +158,9 @@ importers:
react-hook-form: react-hook-form:
specifier: ^7.54.2 specifier: ^7.54.2
version: 7.54.2(react@18.3.1) version: 7.54.2(react@18.3.1)
react-infinite-scroll-component:
specifier: ^6.1.0
version: 6.1.0(react@18.3.1)
react-redux: react-redux:
specifier: ^9.0.4 specifier: ^9.0.4
version: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1) version: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1)
@ -195,6 +198,9 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: ^18.3.5 specifier: ^18.3.5
version: 18.3.5(@types/react@18.3.18) version: 18.3.5(@types/react@18.3.18)
'@types/react-infinite-scroll-component':
specifier: ^5.0.0
version: 5.0.0(react@18.3.1)
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^6.14.0 specifier: ^6.14.0
version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)
@ -2114,6 +2120,10 @@ packages:
peerDependencies: peerDependencies:
'@types/react': ^18.0.0 '@types/react': ^18.0.0
'@types/react-infinite-scroll-component@5.0.0':
resolution: {integrity: sha512-1DrmAOFfiy6nchA/KiXCAEzXxG4VHsEGJGlRyMZYy8A9WGfr0fAVe+MXep+VkjIPnNPf06qDbJGSNiPzt67wJg==}
deprecated: This is a stub types definition. react-infinite-scroll-component provides its own type definitions, so you do not need this installed.
'@types/react-window@1.8.8': '@types/react-window@1.8.8':
resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==}
@ -3658,6 +3668,11 @@ packages:
react-native: react-native:
optional: true optional: true
react-infinite-scroll-component@6.1.0:
resolution: {integrity: sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==}
peerDependencies:
react: '>=16.0.0'
react-is@16.13.1: react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@ -4035,6 +4050,10 @@ packages:
thenify@3.3.1: thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
throttle-debounce@2.3.0:
resolution: {integrity: sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==}
engines: {node: '>=8'}
throttle-debounce@5.0.2: throttle-debounce@5.0.2:
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
engines: {node: '>=12.22'} engines: {node: '>=12.22'}
@ -6366,6 +6385,12 @@ snapshots:
dependencies: dependencies:
'@types/react': 18.3.18 '@types/react': 18.3.18
'@types/react-infinite-scroll-component@5.0.0(react@18.3.1)':
dependencies:
react-infinite-scroll-component: 6.1.0(react@18.3.1)
transitivePeerDependencies:
- react
'@types/react-window@1.8.8': '@types/react-window@1.8.8':
dependencies: dependencies:
'@types/react': 18.3.18 '@types/react': 18.3.18
@ -8122,6 +8147,11 @@ snapshots:
optionalDependencies: optionalDependencies:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
react-infinite-scroll-component@6.1.0(react@18.3.1):
dependencies:
react: 18.3.1
throttle-debounce: 2.3.0
react-is@16.13.1: {} react-is@16.13.1: {}
react-is@18.3.1: {} react-is@18.3.1: {}
@ -8552,6 +8582,8 @@ snapshots:
dependencies: dependencies:
any-promise: 1.3.0 any-promise: 1.3.0
throttle-debounce@2.3.0: {}
throttle-debounce@5.0.2: {} throttle-debounce@5.0.2: {}
tiny-invariant@1.3.3: {} tiny-invariant@1.3.3: {}

View File

@ -0,0 +1,212 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
import InfiniteScroll from 'react-infinite-scroll-component';
import { Loader2 } from 'lucide-react';
import type { Page } from '@/types/base';
interface InfiniteScrollListProps<T> {
/**
*
* @param pageNum - 0
* @param size -
* @param extraParams -
* @returns
*/
loadData: (pageNum: number, size: number, extraParams?: any) => Promise<Page<T> | undefined>;
/**
*
*/
renderItem: (item: T, index: number) => React.ReactNode;
/**
* 20
*/
pageSize?: number;
/**
*
*/
extraParams?: any;
/**
* ID
*/
scrollableTarget: string;
/**
*
*/
className?: string;
/**
*
*/
listClassName?: string;
/**
*
*/
emptyText?: string;
/**
*
*/
emptyIcon?: React.ReactNode;
/**
*
*/
endMessage?: string;
/**
* loading
*/
showInitialLoading?: boolean;
/**
*
*/
resetTrigger?: any;
/**
*
*/
onDataChange?: (data: T[], total: number) => void;
}
export function InfiniteScrollList<T>({
loadData,
renderItem,
pageSize = 20,
extraParams,
scrollableTarget,
className = '',
listClassName = 'p-2 space-y-2',
emptyText = '暂无数据',
emptyIcon,
endMessage = '已加载全部数据',
showInitialLoading = true,
resetTrigger,
onDataChange,
}: InfiniteScrollListProps<T>) {
const [data, setData] = useState<T[]>([]);
const [page, setPage] = useState(0);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(showInitialLoading);
// 使用 ref 跟踪是否正在加载,避免重复请求
const loadingRef = useRef(false);
// 加载数据
const fetchData = useCallback(async (pageNum: number, reset: boolean = false) => {
if (loadingRef.current) return;
loadingRef.current = true;
setLoading(true);
try {
const response = await loadData(pageNum, pageSize, extraParams);
if (response) {
const newData = response.content || [];
if (reset) {
setData(newData);
} else {
setData(prev => [...prev, ...newData]);
}
setPage(pageNum);
setHasMore(!response.last);
// 触发数据变化回调
if (onDataChange) {
const totalData = reset ? newData : [...data, ...newData];
onDataChange(totalData, response.totalElements || 0);
}
}
} catch (error) {
console.error('Failed to load data:', error);
} finally {
setLoading(false);
setInitialLoading(false);
loadingRef.current = false;
}
}, [loadData, pageSize, extraParams, data, onDataChange]);
// 加载更多
const loadMore = useCallback(() => {
if (!loading && hasMore) {
fetchData(page + 1, false);
}
}, [loading, hasMore, page, fetchData]);
// 重置并重新加载
const reset = useCallback(() => {
setData([]);
setPage(0);
setHasMore(true);
setInitialLoading(showInitialLoading);
fetchData(0, true);
}, [fetchData, showInitialLoading]);
// 初始加载
useEffect(() => {
reset();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resetTrigger]);
// 初始 loading 状态
if (initialLoading && data.length === 0) {
return (
<div className={`flex items-center justify-center h-full ${className}`}>
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
);
}
// 空状态
if (!loading && data.length === 0) {
return (
<div className={`flex flex-col items-center justify-center h-full text-muted-foreground ${className}`}>
{emptyIcon || <div className="h-12 w-12 mb-2 opacity-20" />}
<p className="text-sm">{emptyText}</p>
</div>
);
}
// 列表渲染
return (
<InfiniteScroll
dataLength={data.length}
next={loadMore}
hasMore={hasMore}
loader={
<div className="flex items-center justify-center py-4 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
...
</div>
}
endMessage={
data.length > 0 ? (
<div className="flex items-center justify-center py-4 text-sm text-muted-foreground">
{endMessage}
</div>
) : null
}
scrollableTarget={scrollableTarget}
className={listClassName}
>
{data.map((item, index) => (
<React.Fragment key={index}>
{renderItem(item, index)}
</React.Fragment>
))}
</InfiniteScroll>
);
}
// 导出类型供外部使用
export type { InfiniteScrollListProps };

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react'; import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { InfiniteScrollList } from '@/components/ui/infinite-scroll-list';
import { import {
Card, Card,
CardHeader, CardHeader,
@ -87,10 +88,8 @@ const GitManager: React.FC = () => {
const [instances, setInstances] = useState<ExternalSystemResponse[]>([]); const [instances, setInstances] = useState<ExternalSystemResponse[]>([]);
const [selectedInstanceId, setSelectedInstanceId] = useState<number>(); const [selectedInstanceId, setSelectedInstanceId] = useState<number>();
// 数据状态 // 数据状态 - 项目和分支由 InfiniteScrollList 管理
const [groupTree, setGroupTree] = useState<RepositoryGroupResponse[]>([]); const [groupTree, setGroupTree] = useState<RepositoryGroupResponse[]>([]);
const [projects, setProjects] = useState<RepositoryProjectResponse[]>([]);
const [branches, setBranches] = useState<RepositoryBranchResponse[]>([]);
// 选中状态 // 选中状态
const [selectedGroup, setSelectedGroup] = useState<RepositoryGroupResponse>(); const [selectedGroup, setSelectedGroup] = useState<RepositoryGroupResponse>();
@ -113,15 +112,16 @@ const GitManager: React.FC = () => {
}); });
// 搜索过滤 // 搜索过滤 - 仅保留仓库组搜索
const [groupSearch, setGroupSearch] = useState(''); const [groupSearch, setGroupSearch] = useState('');
const [projectSearch, setProjectSearch] = useState('');
const [branchSearch, setBranchSearch] = useState('');
// 防抖搜索值 - 优化大量数据过滤性能 // 防抖搜索值 - 优化大量数据过滤性能
const [debouncedGroupSearch, setDebouncedGroupSearch] = useState(''); const [debouncedGroupSearch, setDebouncedGroupSearch] = useState('');
const [debouncedProjectSearch, setDebouncedProjectSearch] = useState('');
const [debouncedBranchSearch, setDebouncedBranchSearch] = useState(''); // 使用 ref 跟踪初始化状态,避免重复请求
const isInitializedRef = useRef(false);
const groupsLoadedRef = useRef(false);
const projectsLoadedRef = useRef(false);
// 仓库组搜索防抖 // 仓库组搜索防抖
useEffect(() => { useEffect(() => {
@ -131,22 +131,6 @@ const GitManager: React.FC = () => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [groupSearch]); }, [groupSearch]);
// 项目搜索防抖
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedProjectSearch(projectSearch);
}, 300);
return () => clearTimeout(timer);
}, [projectSearch]);
// 分支搜索防抖
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedBranchSearch(branchSearch);
}, 300);
return () => clearTimeout(timer);
}, [branchSearch]);
// 构建树形结构 // 构建树形结构
const buildTree = useCallback((groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => { const buildTree = useCallback((groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => {
// 使用 repoGroupId 作为 key因为 parentId 对应的是 Git 系统中的 repoGroupId // 使用 repoGroupId 作为 key因为 parentId 对应的是 Git 系统中的 repoGroupId
@ -209,48 +193,8 @@ const GitManager: React.FC = () => {
} }
}, [selectedInstanceId, buildTree, toast]); }, [selectedInstanceId, buildTree, toast]);
// 加载项目列表支持不传repoGroupId加载所有项目 // 加载项目列表支持不传repoGroupId加载所有项目- 现在不再需要,由 InfiniteScrollList 处理
const loadProjects = useCallback(async (repoGroupId?: number) => { // 加载分支列表支持不传repoProjectId加载该组所有分支- 现在不再需要,由 InfiniteScrollList 处理
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, projects: true }));
try {
const data = await getRepositoryProjects({
externalSystemId: selectedInstanceId,
repoGroupId,
});
setProjects(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '加载项目列表失败',
});
setProjects([]);
} finally {
setLoading((prev) => ({ ...prev, projects: false }));
}
}, [selectedInstanceId, toast]);
// 加载分支列表支持不传repoProjectId加载该组所有分支
const loadBranches = useCallback(async (repoProjectId?: number) => {
setLoading((prev) => ({ ...prev, branches: true }));
try {
const data = await getRepositoryBranches({
repoProjectId,
});
setBranches(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '加载分支列表失败',
});
setBranches([]);
} finally {
setLoading((prev) => ({ ...prev, branches: false }));
}
}, [toast]);
// 同步所有数据(异步任务) // 同步所有数据(异步任务)
const handleSyncAll = useCallback(async () => { const handleSyncAll = useCallback(async () => {
@ -518,25 +462,9 @@ const GitManager: React.FC = () => {
); );
}; };
// 过滤项目列表(使用防抖搜索值) // 项目和分支列表现在由 InfiniteScrollList 组件内部管理,不再需要客户端过滤
const filteredProjects = useMemo(() => {
if (!debouncedProjectSearch) return projects;
return projects.filter(
(project) =>
project.name.toLowerCase().includes(debouncedProjectSearch.toLowerCase()) ||
(project.path && project.path.toLowerCase().includes(debouncedProjectSearch.toLowerCase()))
);
}, [projects, debouncedProjectSearch]);
// 过滤分支列表(使用防抖搜索值) // 加载 Git 实例列表(只在组件挂载时执行一次)
const filteredBranches = useMemo(() => {
if (!debouncedBranchSearch) return branches;
return branches.filter((branch) =>
branch.name.toLowerCase().includes(debouncedBranchSearch.toLowerCase())
);
}, [branches, debouncedBranchSearch]);
// 加载 Git 实例列表
useEffect(() => { useEffect(() => {
const loadInstancesEffect = async () => { const loadInstancesEffect = async () => {
try { try {
@ -556,38 +484,32 @@ const GitManager: React.FC = () => {
} }
}; };
loadInstancesEffect(); loadInstancesEffect();
}, [selectedInstanceId, toast]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 只在组件挂载时执行一次
// 监听实例变化 // 实例变化时 - 重置状态并加载仓库组
useEffect(() => { useEffect(() => {
if (selectedInstanceId) { if (!selectedInstanceId) return;
loadGroupTree();
// 重置所有状态和标志
isInitializedRef.current = false;
groupsLoadedRef.current = false;
projectsLoadedRef.current = false;
setSelectedGroup(undefined); setSelectedGroup(undefined);
setSelectedProject(undefined); setSelectedProject(undefined);
setProjects([]); // 项目和分支列表由 InfiniteScrollList 自动重置
setBranches([]);
}
}, [selectedInstanceId, loadGroupTree]);
// 监听仓库组加载完成 或 仓库组选择变化,自动加载项目列表 // 加载仓库组列表
loadGroupTree();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedInstanceId]); // 只监听 selectedInstanceId避免重复请求
// 仓库组选择变化 - 清除下级选择InfiniteScrollList 会自动重新加载)
useEffect(() => { useEffect(() => {
// 只有当仓库组不在加载中时才加载项目
if (selectedInstanceId && !loading.groups) {
setSelectedProject(undefined); setSelectedProject(undefined);
setBranches([]); // eslint-disable-next-line react-hooks/exhaustive-deps
// 如果选择了组传递repoGroupId否则不传加载所有项目 }, [selectedGroup?.repoGroupId, selectedGroup?.id]); // 只监听选择的组ID
loadProjects(selectedGroup?.repoGroupId || selectedGroup?.id);
}
}, [loading.groups, selectedInstanceId, selectedGroup, loadProjects]);
// 监听项目加载完成 或 项目选择变化,自动加载分支列表
useEffect(() => {
// 只有当项目不在加载中时才加载分支
if (selectedInstanceId && !loading.projects) {
// 如果选择了项目传递repoProjectId否则不传加载所有分支
loadBranches(selectedProject?.repoProjectId || selectedProject?.id);
}
}, [loading.projects, selectedInstanceId, selectedProject, loadBranches]);
const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, filterGroupTree]); const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, filterGroupTree]);
@ -711,20 +633,9 @@ 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">
({filteredProjects.length}){selectedGroup && ` - ${selectedGroup.name}`} {selectedGroup && ` - ${selectedGroup.name}`}
</CardTitle> </CardTitle>
<div className="flex items-center gap-1"> <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"
@ -738,33 +649,21 @@ const GitManager: React.FC = () => {
</Button> </Button>
</div> </div>
</div> </div>
<div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="搜索项目..."
value={projectSearch}
onChange={(e) => setProjectSearch(e.target.value)}
className="pl-8 h-8"
/>
</div>
</CardHeader> </CardHeader>
<div className="flex-1 overflow-auto"> <div
{loading.projects ? ( id="projectScrollableDiv"
<div className="flex items-center justify-center h-full"> className="flex-1 overflow-auto"
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> >
</div> <InfiniteScrollList
) : filteredProjects.length === 0 ? ( loadData={(pageNum, size) =>
<div className="flex flex-col items-center justify-center h-full text-muted-foreground"> getRepositoryProjects({
<Code2 className="h-12 w-12 mb-2 opacity-20" /> externalSystemId: selectedInstanceId!,
<p className="text-sm">{selectedGroup ? '当前仓库组暂无项目' : '暂无项目'}</p> repoGroupId: selectedGroup?.repoGroupId || selectedGroup?.id,
</div> pageNum,
) : ( size,
})
<div className="p-2 space-y-2"> }
{filteredProjects.map((project) => ( renderItem={(project) => (
<div <div
key={project.id} key={project.id}
className={`p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${ className={`p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${
@ -794,34 +693,29 @@ const GitManager: React.FC = () => {
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<GitBranch className="h-3 w-3" /> <GitBranch className="h-3 w-3" />
{project.isDefaultBranch} {project.isDefaultBranch}
</span> </span>
)} )}
{project.branchCount !== undefined && project.branchCount > 0 && ( {project.branchCount !== undefined && project.branchCount > 0 && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<GitBranch className="h-3 w-3" /> <GitBranch className="h-3 w-3" />
{project.branchCount} {project.branchCount}
</span> </span>
)} )}
{project.lastActivityAt && ( {project.lastActivityAt && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
{formatTime(project.lastActivityAt)} {formatTime(project.lastActivityAt)}
</span> </span>
)} )}
</div> </div>
</div> </div>
{project.webUrl && ( {project.webUrl && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<a <a
href={project.webUrl} href={project.webUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="flex-shrink-0" className="flex-shrink-0"
> >
@ -836,10 +730,14 @@ const GitManager: React.FC = () => {
</Tooltip> </Tooltip>
)} )}
</div> </div>
</div>
))}
</div> </div>
)} )}
scrollableTarget="projectScrollableDiv"
pageSize={20}
resetTrigger={selectedGroup?.id}
emptyText={selectedGroup ? '当前仓库组暂无项目' : '暂无项目'}
className="p-2 space-y-2"
/>
</div> </div>
</Card> </Card>
@ -848,20 +746,9 @@ 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">
({filteredBranches.length}){selectedProject && ` - ${selectedProject.name}`} {selectedProject && ` - ${selectedProject.name}`}
</CardTitle> </CardTitle>
<div className="flex items-center gap-1"> <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"
@ -875,30 +762,22 @@ const GitManager: React.FC = () => {
</Button> </Button>
</div> </div>
</div> </div>
<div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="搜索分支..."
value={branchSearch}
onChange={(e) => setBranchSearch(e.target.value)}
className="pl-8 h-8"
/>
</div>
</CardHeader> </CardHeader>
<div className="flex-1 overflow-auto"> <div
{loading.branches ? ( id="branchScrollableDiv"
<div className="flex items-center justify-center h-full"> className="flex-1 overflow-auto"
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> >
</div> <InfiniteScrollList
) : filteredBranches.length === 0 ? ( loadData={(pageNum, size) =>
<div className="flex flex-col items-center justify-center h-full text-muted-foreground"> getRepositoryBranches({
<GitBranch className="h-12 w-12 mb-2 opacity-20" /> externalSystemId: selectedInstanceId!,
<p className="text-sm">{selectedProject ? '当前项目暂无分支' : '暂无分支'}</p> repoProjectId: selectedProject?.repoProjectId || selectedProject?.id,
</div> pageNum,
) : ( size,
<div className="divide-y"> })
{filteredBranches.map((branch) => ( }
<div key={branch.id} className="p-3 hover:bg-accent transition-colors"> renderItem={(branch) => (
<div key={branch.id} className="p-3 hover:bg-accent transition-colors border-b">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<GitBranch className="h-4 w-4 mt-0.5 flex-shrink-0 text-muted-foreground" /> <GitBranch className="h-4 w-4 mt-0.5 flex-shrink-0 text-muted-foreground" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@ -955,7 +834,6 @@ const GitManager: React.FC = () => {
<ExternalLink className="h-3 w-3" /> <ExternalLink className="h-3 w-3" />
</Button> </Button>
</a> </a>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p> GitLab </p> <p> GitLab </p>
@ -963,10 +841,14 @@ const GitManager: React.FC = () => {
</Tooltip> </Tooltip>
)} )}
</div> </div>
</div>
))}
</div> </div>
)} )}
scrollableTarget="branchScrollableDiv"
pageSize={20}
resetTrigger={selectedProject?.id}
emptyText={selectedProject ? '当前项目暂无分支' : '暂无分支'}
className=""
/>
</div> </div>
</Card> </Card>
</div> </div>

View File

@ -3,12 +3,11 @@ import type {
RepositoryGroupResponse, RepositoryGroupResponse,
RepositoryGroupQuery, RepositoryGroupQuery,
RepositoryProjectResponse, RepositoryProjectResponse,
RepositoryProjectQuery,
RepositoryBranchResponse, RepositoryBranchResponse,
RepositoryBranchQuery,
} from './types'; } from './types';
import { getExternalSystems } from '@/pages/Deploy/External/service'; import { getExternalSystems } from '@/pages/Deploy/External/service';
import { SystemType } from '@/pages/Deploy/External/types'; import { SystemType } from '@/pages/Deploy/External/types';
import type { Page } from '@/types/base';
// API 基础路径 // API 基础路径
const GROUP_URL = '/api/v1/repository-group'; const GROUP_URL = '/api/v1/repository-group';
@ -45,17 +44,21 @@ export const syncRepositoryGroups = (externalSystemId: number) =>
// ==================== 仓库项目 ==================== // ==================== 仓库项目 ====================
/** /**
* *
*/ */
export const getRepositoryProjects = (params?: RepositoryProjectQuery) => export const getRepositoryProjects = (params: {
request.get<RepositoryProjectResponse[]>(`${PROJECT_URL}/list`, { params }); externalSystemId: number;
repoGroupId?: number;
/** pageNum?: number;
* ID获取项目列表 size?: number;
*/ }) =>
export const getProjectsByGroupId = (repoGroupId: number) => request.get<Page<RepositoryProjectResponse>>(`${PROJECT_URL}/page`, {
request.get<RepositoryProjectResponse[]>(`${PROJECT_URL}/list`, { params: {
params: { repoGroupId }, pageNum: params.pageNum ?? 0,
size: params.size ?? 20,
externalSystemId: params.externalSystemId,
repoGroupId: params.repoGroupId,
},
}); });
/** /**
@ -71,26 +74,22 @@ export const syncRepositoryProjects = (externalSystemId: number, repoGroupId?: n
// ==================== 仓库分支 ==================== // ==================== 仓库分支 ====================
/** /**
* *
*/ */
export const getRepositoryBranches = (params?: RepositoryBranchQuery) => export const getRepositoryBranches = (params: {
request.get<RepositoryBranchResponse[]>(`${BRANCH_URL}/list`, { externalSystemId: number;
repoProjectId?: number;
pageNum?: number;
size?: number;
}) =>
request.get<Page<RepositoryBranchResponse>>(`${BRANCH_URL}/page`, {
params: { params: {
...params, pageNum: params.pageNum ?? 0,
size: params.size ?? 20,
sortField: 'lastUpdateTime', sortField: 'lastUpdateTime',
sortOrder: 'desc' sortOrder: 'desc',
} externalSystemId: params.externalSystemId,
}); repoProjectId: params.repoProjectId,
/**
* ID获取分支列表
*/
export const getBranchesByProjectId = (repoProjectId: number) =>
request.get<RepositoryBranchResponse[]>(`${BRANCH_URL}/list`, {
params: {
repoProjectId,
sortField: 'lastUpdateTime',
sortOrder: 'desc'
}, },
}); });

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import InfiniteScroll from 'react-infinite-scroll-component';
import { import {
Card, Card,
CardHeader, CardHeader,
@ -54,7 +55,6 @@ 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';
@ -88,6 +88,12 @@ const JenkinsManager: React.FC = () => {
const [jobs, setJobs] = useState<JenkinsJobDTO[]>([]); const [jobs, setJobs] = useState<JenkinsJobDTO[]>([]);
const [builds, setBuilds] = useState<JenkinsBuildDTO[]>([]); const [builds, setBuilds] = useState<JenkinsBuildDTO[]>([]);
// 分页状态
const [jobPage, setJobPage] = useState(0);
const [jobHasMore, setJobHasMore] = useState(true);
const [buildPage, setBuildPage] = useState(0);
const [buildHasMore, setBuildHasMore] = useState(true);
// 选中状态 // 选中状态
const [selectedView, setSelectedView] = useState<JenkinsViewDTO>(); const [selectedView, setSelectedView] = useState<JenkinsViewDTO>();
const [selectedJob, setSelectedJob] = useState<JenkinsJobDTO>(); const [selectedJob, setSelectedJob] = useState<JenkinsJobDTO>();
@ -116,6 +122,11 @@ const JenkinsManager: React.FC = () => {
const [debouncedJobSearch, setDebouncedJobSearch] = useState(''); const [debouncedJobSearch, setDebouncedJobSearch] = useState('');
const [debouncedBuildSearch, setDebouncedBuildSearch] = useState(''); const [debouncedBuildSearch, setDebouncedBuildSearch] = useState('');
// 使用 ref 跟踪初始化状态,避免重复请求
const isInitializedRef = useRef(false);
const viewsLoadedRef = useRef(false);
const jobsLoadedRef = useRef(false);
// 视图搜索防抖 // 视图搜索防抖
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@ -158,16 +169,27 @@ const JenkinsManager: React.FC = () => {
} }
}, [selectedInstanceId, toast]); }, [selectedInstanceId, toast]);
// 加载任务列表 // 加载任务列表(首次加载或重置)
const loadJobs = useCallback(async (viewId?: number) => { const loadJobs = useCallback(async (viewId?: number, reset: boolean = true) => {
if (!selectedInstanceId) return; if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, jobs: true })); setLoading((prev) => ({ ...prev, jobs: true }));
try { try {
const data = await getJenkinsJobs({ const pageNum = reset ? 0 : jobPage;
const response = await getJenkinsJobs({
externalSystemId: selectedInstanceId, externalSystemId: selectedInstanceId,
viewId, viewId,
pageNum,
size: 20,
}); });
setJobs(data || []); if (response) {
if (reset) {
setJobs(response.content || []);
setJobPage(0);
} else {
setJobs((prev) => [...prev, ...(response.content || [])]);
}
setJobHasMore(!response.last);
}
} catch (error) { } catch (error) {
toast({ toast({
variant: 'destructive', variant: 'destructive',
@ -177,18 +199,57 @@ const JenkinsManager: React.FC = () => {
} finally { } finally {
setLoading((prev) => ({ ...prev, jobs: false })); setLoading((prev) => ({ ...prev, jobs: false }));
} }
}, [selectedInstanceId, toast]); }, [selectedInstanceId, jobPage, toast]);
// 加载构建列表 // 加载更多任务
const loadBuilds = useCallback(async (jobId?: number) => { 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; if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, builds: true })); setLoading((prev) => ({ ...prev, builds: true }));
try { try {
const data = await getJenkinsBuilds({ const pageNum = reset ? 0 : buildPage;
const response = await getJenkinsBuilds({
externalSystemId: selectedInstanceId, externalSystemId: selectedInstanceId,
jobId, jobId,
pageNum,
size: 20,
}); });
setBuilds(data || []); if (response) {
if (reset) {
setBuilds(response.content || []);
setBuildPage(0);
} else {
setBuilds((prev) => [...prev, ...(response.content || [])]);
}
setBuildHasMore(!response.last);
}
} catch (error) { } catch (error) {
toast({ toast({
variant: 'destructive', variant: 'destructive',
@ -198,9 +259,37 @@ const JenkinsManager: React.FC = () => {
} finally { } finally {
setLoading((prev) => ({ ...prev, builds: false })); setLoading((prev) => ({ ...prev, builds: false }));
} }
}, [selectedInstanceId, toast]); }, [selectedInstanceId, buildPage, toast]);
// 加载 Jenkins 实例列表 // 加载更多构建
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 实例列表(只在组件挂载时执行一次)
useEffect(() => { useEffect(() => {
const loadInstances = async () => { const loadInstances = async () => {
try { try {
@ -218,7 +307,8 @@ const JenkinsManager: React.FC = () => {
} }
}; };
loadInstances(); loadInstances();
}, [selectedInstanceId, toast]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 只在组件挂载时执行一次
// 同步视图 // 同步视图
const handleSyncViews = useCallback(async () => { const handleSyncViews = useCallback(async () => {
@ -298,36 +388,81 @@ const JenkinsManager: React.FC = () => {
} }
}, [selectedInstanceId, selectedJob, toast, loadBuilds]); }, [selectedInstanceId, selectedJob, toast, loadBuilds]);
// 监听实例变化 // 实例变化时 - 重置状态并加载视图
useEffect(() => { useEffect(() => {
if (selectedInstanceId) { if (!selectedInstanceId) return;
// 重置所有状态和标志
isInitializedRef.current = false;
viewsLoadedRef.current = false;
jobsLoadedRef.current = false;
setSelectedView(undefined); setSelectedView(undefined);
setSelectedJob(undefined); setSelectedJob(undefined);
setJobs([]); setJobs([]);
setBuilds([]); setBuilds([]);
setJobPage(0);
setJobHasMore(true);
setBuildPage(0);
setBuildHasMore(true);
// 加载视图列表
loadViews(); loadViews();
} // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedInstanceId, loadViews]); }, [selectedInstanceId]); // 只监听 selectedInstanceId避免重复请求
// 监听视图加载完成 或 视图选择变化,自动加载任务列表 // 视图加载完成后 - 自动加载任务(只执行一次)
useEffect(() => { useEffect(() => {
// 只有当视图不在加载中时才加载任务 if (selectedInstanceId && views.length > 0 && !viewsLoadedRef.current) {
if (selectedInstanceId && !loading.views) { viewsLoadedRef.current = true;
loadJobs(undefined, true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedInstanceId, views.length]); // 监听 views.length 变化
// 任务加载完成后 - 自动加载构建(只执行一次)
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); setSelectedJob(undefined);
setJobs([]); // 🔥 关键:立即清空任务数据
setBuilds([]); setBuilds([]);
// 如果选择了视图传递viewId否则不传加载所有任务 setJobPage(0);
loadJobs(selectedView?.id); setJobHasMore(true);
} setBuildPage(0);
}, [loading.views, selectedInstanceId, selectedView, loadJobs]); setBuildHasMore(true);
// 监听任务加载完成 或 任务选择变化,自动加载构建列表 // 重新加载任务
loadJobs(selectedView?.id, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedView?.id]); // 只监听 selectedView?.id
// 任务选择变化 - 重新加载构建
useEffect(() => { useEffect(() => {
// 只有当任务不在加载中时才加载构建 // 只有初始化完成后才处理选择变化
if (selectedInstanceId && !loading.jobs) { if (!isInitializedRef.current) return;
// 如果选择了任务传递jobId否则不传加载所有构建
loadBuilds(selectedJob?.id); // 立即清空旧数据,避免渲染大量过时数据导致卡顿
} setBuilds([]); // 🔥 关键:立即清空构建数据
}, [loading.jobs, selectedInstanceId, selectedJob, loadBuilds]); setBuildPage(0);
setBuildHasMore(true);
// 重新加载构建
loadBuilds(selectedJob?.id, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedJob?.id]); // 只监听 selectedJob?.id
// 过滤视图(使用防抖搜索值) // 过滤视图(使用防抖搜索值)
const filteredViews = useMemo(() => { const filteredViews = useMemo(() => {
@ -378,14 +513,6 @@ const JenkinsManager: React.FC = () => {
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>
@ -566,8 +693,11 @@ const JenkinsManager: React.FC = () => {
/> />
</div> </div>
</CardHeader> </CardHeader>
<div className="flex-1 overflow-auto"> <div
{loading.jobs ? ( id="jobScrollableDiv"
className="flex-1 overflow-auto"
>
{loading.jobs && filteredJobs.length === 0 ? (
<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>
@ -577,7 +707,24 @@ const JenkinsManager: React.FC = () => {
<p className="text-sm">{selectedView ? '当前视图暂无任务' : '暂无任务'}</p> <p className="text-sm">{selectedView ? '当前视图暂无任务' : '暂无任务'}</p>
</div> </div>
) : ( ) : (
<div className="p-2 space-y-2"> <InfiniteScroll
dataLength={filteredJobs.length}
next={loadMoreJobs}
hasMore={jobHasMore}
loader={
<div className="flex items-center justify-center py-4 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
...
</div>
}
endMessage={
<div className="flex items-center justify-center py-4 text-sm text-muted-foreground">
</div>
}
scrollableTarget="jobScrollableDiv"
className="p-2 space-y-2"
>
{filteredJobs.map((job) => ( {filteredJobs.map((job) => (
<div <div
key={job.id} key={job.id}
@ -654,7 +801,7 @@ const JenkinsManager: React.FC = () => {
)} )}
</div> </div>
))} ))}
</div> </InfiniteScroll>
)} )}
</div> </div>
</Card> </Card>
@ -702,10 +849,10 @@ const JenkinsManager: React.FC = () => {
</div> </div>
</CardHeader> </CardHeader>
<div <div
ref={buildListRef} id="buildScrollableDiv"
className="flex-1 overflow-auto" className="flex-1 overflow-auto"
> >
{loading.builds ? ( {loading.builds && filteredBuilds.length === 0 ? (
<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>
@ -715,28 +862,28 @@ const JenkinsManager: React.FC = () => {
<p className="text-sm">{selectedJob ? '当前任务暂无构建' : '暂无构建'}</p> <p className="text-sm">{selectedJob ? '当前任务暂无构建' : '暂无构建'}</p>
</div> </div>
) : ( ) : (
<div <InfiniteScroll
style={{ dataLength={filteredBuilds.length}
height: `${buildVirtualizer.getTotalSize()}px`, next={loadMoreBuilds}
width: '100%', hasMore={buildHasMore}
position: 'relative', loader={
}} <div className="flex items-center justify-center py-4 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
...
</div>
}
endMessage={
<div className="flex items-center justify-center py-4 text-sm text-muted-foreground">
</div>
}
scrollableTarget="buildScrollableDiv"
className="p-2 space-y-2"
> >
{buildVirtualizer.getVirtualItems().map((virtualItem) => { {filteredBuilds.map((build) => (
const build = filteredBuilds[virtualItem.index];
return (
<div <div
key={build.id} key={build.id}
data-index={virtualItem.index} className="group p-3 hover:bg-accent transition-colors border rounded-lg"
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">
@ -780,9 +927,8 @@ const JenkinsManager: React.FC = () => {
</div> </div>
)} )}
</div> </div>
); ))}
})} </InfiniteScroll>
</div>
)} )}
</div> </div>
</Card> </Card>

View File

@ -2,6 +2,7 @@ import request from '@/utils/request';
import type { JenkinsViewDTO, JenkinsJobDTO, JenkinsBuildDTO } from './types'; import type { JenkinsViewDTO, JenkinsJobDTO, JenkinsBuildDTO } from './types';
import { getExternalSystems } from '@/pages/Deploy/External/service'; import { getExternalSystems } from '@/pages/Deploy/External/service';
import { SystemType } from '@/pages/Deploy/External/types'; import { SystemType } from '@/pages/Deploy/External/types';
import type { Page } from '@/types/base';
// 获取 Jenkins 实例列表 // 获取 Jenkins 实例列表
export const getJenkinsInstances = () => export const getJenkinsInstances = () =>
@ -26,9 +27,21 @@ export const syncJenkinsViews = (externalSystemId: number) =>
// ==================== Jenkins 任务 ==================== // ==================== Jenkins 任务 ====================
// 获取任务列表 // 获取任务列表(分页)
export const getJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) => export const getJenkinsJobs = (params: {
request.get<JenkinsJobDTO[]>(`/api/v1/jenkins-job/list`, { params }); externalSystemId: number;
viewId?: number;
pageNum?: number;
size?: number;
}) =>
request.get<Page<JenkinsJobDTO>>(`/api/v1/jenkins-job/page`, {
params: {
pageNum: params.pageNum ?? 0,
size: params.size ?? 20,
externalSystemId: params.externalSystemId,
viewId: params.viewId
}
});
// 同步任务 // 同步任务
export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) => export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) =>
@ -36,13 +49,21 @@ export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: num
// ==================== Jenkins 构建 ==================== // ==================== Jenkins 构建 ====================
// 获取构建列表(按开始时间倒序排序) // 获取构建列表(分页,按开始时间倒序排序)
export const getJenkinsBuilds = (params: { externalSystemId: number; jobId?: number }) => export const getJenkinsBuilds = (params: {
request.get<JenkinsBuildDTO[]>(`/api/v1/jenkins-build/list`, { externalSystemId: number;
jobId?: number;
pageNum?: number;
size?: number;
}) =>
request.get<Page<JenkinsBuildDTO>>(`/api/v1/jenkins-build/page`, {
params: { params: {
...params, pageNum: params.pageNum ?? 0,
size: params.size ?? 20,
sortField: 'starttime', sortField: 'starttime',
sortOrder: 'desc' sortOrder: 'desc',
externalSystemId: params.externalSystemId,
jobId: params.jobId
} }
}); });