重构前端逻辑
This commit is contained in:
parent
d9807a1816
commit
011e13dbfa
227
frontend/src/components/ui/confirm-dialog.tsx
Normal file
227
frontend/src/components/ui/confirm-dialog.tsx
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export interface ConfirmDialogProps<T = any> {
|
||||||
|
/** 对话框开关状态 */
|
||||||
|
open: boolean;
|
||||||
|
/** 对话框状态变化回调 */
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
/** 对话框标题 */
|
||||||
|
title: string | React.ReactNode;
|
||||||
|
/** 对话框描述文字(可选) */
|
||||||
|
description?: string | React.ReactNode;
|
||||||
|
/** 自定义内容区域(优先级高于 description) */
|
||||||
|
children?: React.ReactNode;
|
||||||
|
|
||||||
|
/** 传递给回调的数据项(可选) */
|
||||||
|
item?: T;
|
||||||
|
/** 确认操作的异步函数 */
|
||||||
|
onConfirm: (item?: T) => Promise<void>;
|
||||||
|
/** 成功后的回调(可选) */
|
||||||
|
onSuccess?: (item?: T) => void;
|
||||||
|
/** 失败后的回调(可选) */
|
||||||
|
onError?: (error: any, item?: T) => void;
|
||||||
|
/** 取消操作的回调(可选) */
|
||||||
|
onCancel?: () => void;
|
||||||
|
|
||||||
|
/** 确认前的验证函数(返回 false 阻止提交) */
|
||||||
|
beforeConfirm?: (item?: T) => boolean | Promise<boolean>;
|
||||||
|
|
||||||
|
/** 确认按钮文字 */
|
||||||
|
confirmText?: string;
|
||||||
|
/** 取消按钮文字 */
|
||||||
|
cancelText?: string;
|
||||||
|
/** 按钮样式变体 */
|
||||||
|
variant?: 'default' | 'destructive' | 'success';
|
||||||
|
|
||||||
|
/** 成功后是否自动关闭(默认 true) */
|
||||||
|
autoCloseOnSuccess?: boolean;
|
||||||
|
/** 失败后是否自动关闭(默认 true,配合 request.ts 拦截器) */
|
||||||
|
autoCloseOnError?: boolean;
|
||||||
|
/** loading 时是否禁用取消按钮(默认 true) */
|
||||||
|
disableCancelWhenLoading?: boolean;
|
||||||
|
|
||||||
|
/** 自定义最大宽度 */
|
||||||
|
maxWidth?: 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
/** 自定义类名 */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认对话框组件
|
||||||
|
*
|
||||||
|
* 基于 shadcn/ui 的 AlertDialog 封装,提供异步操作的完整支持
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* // 简单删除确认
|
||||||
|
* <ConfirmDialog
|
||||||
|
* open={open}
|
||||||
|
* onOpenChange={setOpen}
|
||||||
|
* title="确认删除"
|
||||||
|
* description="此操作不可撤销"
|
||||||
|
* onConfirm={async () => await deleteAPI()}
|
||||||
|
* onSuccess={() => toast({ title: '删除成功' })}
|
||||||
|
* variant="destructive"
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* // 复杂内容 + 自定义
|
||||||
|
* <ConfirmDialog
|
||||||
|
* open={open}
|
||||||
|
* onOpenChange={setOpen}
|
||||||
|
* title="确认删除"
|
||||||
|
* item={deletingItem}
|
||||||
|
* onConfirm={async (item) => await deleteTeam(item.id)}
|
||||||
|
* variant="destructive"
|
||||||
|
* >
|
||||||
|
* <div className="space-y-3">
|
||||||
|
* <Alert variant="destructive">
|
||||||
|
* <AlertDescription>该团队还有成员</AlertDescription>
|
||||||
|
* </Alert>
|
||||||
|
* </div>
|
||||||
|
* </ConfirmDialog>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function ConfirmDialog<T = any>({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
item,
|
||||||
|
onConfirm,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
onCancel,
|
||||||
|
beforeConfirm,
|
||||||
|
confirmText = '确定',
|
||||||
|
cancelText = '取消',
|
||||||
|
variant = 'default',
|
||||||
|
autoCloseOnSuccess = true,
|
||||||
|
autoCloseOnError = true,
|
||||||
|
disableCancelWhenLoading = true,
|
||||||
|
maxWidth = 'md',
|
||||||
|
className,
|
||||||
|
}: ConfirmDialogProps<T>) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修复 Radix UI AlertDialog 关闭后 body.style.pointerEvents 残留的问题
|
||||||
|
* 当对话框从打开变为关闭时,自动清理 body 样式
|
||||||
|
*/
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (open === false) {
|
||||||
|
// 延迟执行,确保在 AlertDialog 关闭动画之后
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
// 强制移除 body 的 pointer-events 限制
|
||||||
|
if (document.body.style.pointerEvents === 'none') {
|
||||||
|
document.body.style.pointerEvents = '';
|
||||||
|
}
|
||||||
|
}, 350); // 稍微比 AlertDialog 动画时间(200ms)长一点
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
// 前置验证
|
||||||
|
if (beforeConfirm) {
|
||||||
|
try {
|
||||||
|
const canProceed = await beforeConfirm(item);
|
||||||
|
if (!canProceed) {
|
||||||
|
return; // 验证失败,不继续执行
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('前置验证失败:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await onConfirm(item);
|
||||||
|
|
||||||
|
// 成功后自动关闭
|
||||||
|
if (autoCloseOnSuccess) {
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行成功回调
|
||||||
|
onSuccess?.(item);
|
||||||
|
} catch (error) {
|
||||||
|
// 失败后自动关闭(配合 request.ts 拦截器显示错误)
|
||||||
|
if (autoCloseOnError) {
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行失败回调
|
||||||
|
onError?.(error, item);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
onCancel?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 按钮样式映射
|
||||||
|
const variantStyles = {
|
||||||
|
default: '',
|
||||||
|
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
|
success: 'bg-green-600 hover:bg-green-700 text-white',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 最大宽度映射
|
||||||
|
const maxWidthStyles = {
|
||||||
|
sm: 'max-w-sm',
|
||||||
|
md: 'max-w-md',
|
||||||
|
lg: 'max-w-lg',
|
||||||
|
xl: 'max-w-xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent className={cn(maxWidthStyles[maxWidth], className)}>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
|
{!children && description && (
|
||||||
|
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||||
|
)}
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
{/* 自定义内容区域 */}
|
||||||
|
{children && <div className="py-4">{children}</div>}
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel
|
||||||
|
disabled={disableCancelWhenLoading && loading}
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
{cancelText}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={loading}
|
||||||
|
className={cn(variantStyles[variant])}
|
||||||
|
>
|
||||||
|
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
{confirmText}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -6,7 +6,7 @@ import type { Page } from '@/types/base';
|
|||||||
interface InfiniteScrollListProps<T> {
|
interface InfiniteScrollListProps<T> {
|
||||||
/**
|
/**
|
||||||
* 数据加载函数
|
* 数据加载函数
|
||||||
* @param pageNum - 页码(从0开始)
|
* @param pageNum - 页码(从 1 开始)
|
||||||
* @param size - 每页大小
|
* @param size - 每页大小
|
||||||
* @param extraParams - 额外参数
|
* @param extraParams - 额外参数
|
||||||
* @returns 分页数据
|
* @returns 分页数据
|
||||||
@ -15,8 +15,11 @@ interface InfiniteScrollListProps<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染单个数据项
|
* 渲染单个数据项
|
||||||
|
* @param item - 数据项
|
||||||
|
* @param index - 索引
|
||||||
|
* @param isSelected - 是否选中
|
||||||
*/
|
*/
|
||||||
renderItem: (item: T, index: number) => React.ReactNode;
|
renderItem: (item: T, index: number, isSelected: boolean) => React.ReactNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每页大小,默认 20
|
* 每页大小,默认 20
|
||||||
@ -53,16 +56,6 @@ interface InfiniteScrollListProps<T> {
|
|||||||
*/
|
*/
|
||||||
emptyIcon?: React.ReactNode;
|
emptyIcon?: React.ReactNode;
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义空状态组件
|
|
||||||
*/
|
|
||||||
emptyState?: React.ReactNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义初始加载占位符
|
|
||||||
*/
|
|
||||||
loadingPlaceholder?: React.ReactNode;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载完成提示文字
|
* 加载完成提示文字
|
||||||
*/
|
*/
|
||||||
@ -82,6 +75,33 @@ interface InfiniteScrollListProps<T> {
|
|||||||
* 数据变化回调
|
* 数据变化回调
|
||||||
*/
|
*/
|
||||||
onDataChange?: (data: T[], total: number) => void;
|
onDataChange?: (data: T[], total: number) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索关键词(外部传入,组件内部会做防抖处理)
|
||||||
|
*/
|
||||||
|
searchValue?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地过滤函数(对已加载的数据进行过滤)
|
||||||
|
* @param item - 数据项
|
||||||
|
* @param searchValue - 防抖后的搜索值
|
||||||
|
*/
|
||||||
|
localFilter?: (item: T, searchValue: string) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中的项
|
||||||
|
*/
|
||||||
|
selectedItem?: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击项的回调
|
||||||
|
*/
|
||||||
|
onItemClick?: (item: T) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于识别项的唯一键(用于判断是否选中)
|
||||||
|
*/
|
||||||
|
itemKey?: keyof T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfiniteScrollList<T>({
|
export function InfiniteScrollList<T>({
|
||||||
@ -98,16 +118,32 @@ export function InfiniteScrollList<T>({
|
|||||||
showInitialLoading = true,
|
showInitialLoading = true,
|
||||||
resetTrigger,
|
resetTrigger,
|
||||||
onDataChange,
|
onDataChange,
|
||||||
|
searchValue = '',
|
||||||
|
localFilter,
|
||||||
|
selectedItem,
|
||||||
|
onItemClick,
|
||||||
|
itemKey,
|
||||||
}: InfiniteScrollListProps<T>) {
|
}: InfiniteScrollListProps<T>) {
|
||||||
const [data, setData] = useState<T[]>([]);
|
const [data, setData] = useState<T[]>([]);
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(1); // ✅ 从 1 开始
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [initialLoading, setInitialLoading] = useState(showInitialLoading);
|
const [initialLoading, setInitialLoading] = useState(showInitialLoading);
|
||||||
|
|
||||||
|
// ✅ 搜索防抖(300ms)
|
||||||
|
const [debouncedSearchValue, setDebouncedSearchValue] = useState(searchValue);
|
||||||
|
|
||||||
// 使用 ref 跟踪是否正在加载,避免重复请求
|
// 使用 ref 跟踪是否正在加载,避免重复请求
|
||||||
const loadingRef = useRef(false);
|
const loadingRef = useRef(false);
|
||||||
|
|
||||||
|
// ✅ 搜索防抖处理
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedSearchValue(searchValue);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchValue]);
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const fetchData = useCallback(async (pageNum: number, reset: boolean = false) => {
|
const fetchData = useCallback(async (pageNum: number, reset: boolean = false) => {
|
||||||
if (loadingRef.current) return;
|
if (loadingRef.current) return;
|
||||||
@ -155,17 +191,29 @@ export function InfiniteScrollList<T>({
|
|||||||
// 重置并重新加载
|
// 重置并重新加载
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setData([]);
|
setData([]);
|
||||||
setPage(0);
|
setPage(1); // ✅ 重置为 1
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
setInitialLoading(showInitialLoading);
|
setInitialLoading(showInitialLoading);
|
||||||
fetchData(0, true);
|
fetchData(1, true); // ✅ 从第 1 页开始
|
||||||
}, [fetchData, showInitialLoading]);
|
}, [fetchData, showInitialLoading]);
|
||||||
|
|
||||||
// 初始加载
|
// 当 resetTrigger 或 debouncedSearchValue 变化时重置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset();
|
reset();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [resetTrigger]);
|
}, [resetTrigger, debouncedSearchValue]);
|
||||||
|
|
||||||
|
// ✅ 本地过滤数据
|
||||||
|
const filteredData = React.useMemo(() => {
|
||||||
|
if (!localFilter || !debouncedSearchValue) return data;
|
||||||
|
return data.filter(item => localFilter(item, debouncedSearchValue));
|
||||||
|
}, [data, localFilter, debouncedSearchValue]);
|
||||||
|
|
||||||
|
// ✅ 判断项是否被选中
|
||||||
|
const isSelected = (item: T) => {
|
||||||
|
if (!selectedItem || !itemKey) return false;
|
||||||
|
return selectedItem[itemKey] === item[itemKey];
|
||||||
|
};
|
||||||
|
|
||||||
// 初始 loading 状态
|
// 初始 loading 状态
|
||||||
if (initialLoading && data.length === 0) {
|
if (initialLoading && data.length === 0) {
|
||||||
@ -177,7 +225,7 @@ export function InfiniteScrollList<T>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 空状态
|
// 空状态
|
||||||
if (!loading && data.length === 0) {
|
if (!loading && filteredData.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col items-center justify-center h-full text-muted-foreground ${className}`}>
|
<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" />}
|
{emptyIcon || <div className="h-12 w-12 mb-2 opacity-20" />}
|
||||||
@ -189,7 +237,7 @@ export function InfiniteScrollList<T>({
|
|||||||
// 列表渲染
|
// 列表渲染
|
||||||
return (
|
return (
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
dataLength={data.length}
|
dataLength={filteredData.length}
|
||||||
next={loadMore}
|
next={loadMore}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
loader={
|
loader={
|
||||||
@ -199,7 +247,7 @@ export function InfiniteScrollList<T>({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
endMessage={
|
endMessage={
|
||||||
data.length > 0 ? (
|
filteredData.length > 0 ? (
|
||||||
<div className="flex items-center justify-center py-4 text-sm text-muted-foreground">
|
<div className="flex items-center justify-center py-4 text-sm text-muted-foreground">
|
||||||
{endMessage}
|
{endMessage}
|
||||||
</div>
|
</div>
|
||||||
@ -208,15 +256,21 @@ export function InfiniteScrollList<T>({
|
|||||||
scrollableTarget={scrollableTarget}
|
scrollableTarget={scrollableTarget}
|
||||||
className={className || listClassName}
|
className={className || listClassName}
|
||||||
>
|
>
|
||||||
{data.map((item, index) => (
|
{filteredData.map((item, index) => {
|
||||||
<React.Fragment key={index}>
|
const selected = isSelected(item);
|
||||||
{renderItem(item, index)}
|
return (
|
||||||
</React.Fragment>
|
<div
|
||||||
))}
|
key={itemKey ? String(item[itemKey]) : index}
|
||||||
|
onClick={() => onItemClick?.(item)}
|
||||||
|
className={onItemClick ? 'cursor-pointer' : ''}
|
||||||
|
>
|
||||||
|
{renderItem(item, index, selected)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出类型供外部使用
|
// 导出类型供外部使用
|
||||||
export type { InfiniteScrollListProps };
|
export type { InfiniteScrollListProps };
|
||||||
|
|
||||||
|
|||||||
@ -5,16 +5,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@ -197,25 +188,7 @@ const ApplicationManageDialog: React.FC<ApplicationManageDialogProps> = ({
|
|||||||
// 确认删除
|
// 确认删除
|
||||||
const handleDeleteConfirm = async () => {
|
const handleDeleteConfirm = async () => {
|
||||||
if (!deleteRecord) return;
|
if (!deleteRecord) return;
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteTeamApplication(deleteRecord.id);
|
await deleteTeamApplication(deleteRecord.id);
|
||||||
toast({
|
|
||||||
title: "删除成功",
|
|
||||||
description: `应用 "${deleteRecord.applicationName}" 已移除`,
|
|
||||||
});
|
|
||||||
setDeleteDialogOpen(false);
|
|
||||||
setDeleteRecord(null);
|
|
||||||
loadApplications();
|
|
||||||
onSuccess?.();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除失败:', error);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "删除失败",
|
|
||||||
description: error instanceof Error ? error.message : '未知错误',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 过滤出未关联的应用
|
// 过滤出未关联的应用
|
||||||
@ -401,24 +374,25 @@ const ApplicationManageDialog: React.FC<ApplicationManageDialogProps> = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* 删除确认对话框 */}
|
{/* 删除确认对话框 */}
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<ConfirmDialog
|
||||||
<AlertDialogContent>
|
open={deleteDialogOpen}
|
||||||
<AlertDialogHeader>
|
onOpenChange={setDeleteDialogOpen}
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
title="确认删除"
|
||||||
<AlertDialogDescription>
|
description={`确定要将应用 "${deleteRecord?.applicationName}" 从团队中移除吗?此操作无法撤销。`}
|
||||||
确定要将应用 "{deleteRecord?.applicationName}" 从团队中移除吗?此操作无法撤销。
|
item={deleteRecord}
|
||||||
</AlertDialogDescription>
|
onConfirm={handleDeleteConfirm}
|
||||||
</AlertDialogHeader>
|
onSuccess={() => {
|
||||||
<AlertDialogFooter>
|
toast({
|
||||||
<AlertDialogCancel onClick={() => setDeleteDialogOpen(false)}>
|
title: "删除成功",
|
||||||
取消
|
description: `应用 "${deleteRecord?.applicationName}" 已移除`,
|
||||||
</AlertDialogCancel>
|
});
|
||||||
<AlertDialogAction onClick={handleDeleteConfirm} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
setDeleteRecord(null);
|
||||||
确认删除
|
loadApplications();
|
||||||
</AlertDialogAction>
|
onSuccess?.();
|
||||||
</AlertDialogFooter>
|
}}
|
||||||
</AlertDialogContent>
|
variant="destructive"
|
||||||
</AlertDialog>
|
confirmText="确认删除"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,15 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
import { deleteTeam } from '../service';
|
import { deleteTeam } from '../service';
|
||||||
import type { TeamResponse } from '../types';
|
import type { TeamResponse } from '../types';
|
||||||
|
|
||||||
@ -33,38 +25,36 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
|||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
if (!record) return;
|
if (!record) return;
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteTeam(record.id);
|
await deleteTeam(record.id);
|
||||||
toast({ title: '删除成功', description: `团队 "${record.teamName}" 已删除` });
|
|
||||||
onSuccess();
|
|
||||||
onOpenChange(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除失败:', error);
|
|
||||||
toast({
|
|
||||||
title: '删除失败',
|
|
||||||
description: error instanceof Error ? error.message : '未知错误',
|
|
||||||
variant: 'destructive'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<ConfirmDialog
|
||||||
<DialogContent className="max-w-md">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle className="flex items-center gap-2">
|
title={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<AlertCircle className="h-5 w-5 text-destructive" />
|
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||||
确认删除
|
确认删除
|
||||||
</DialogTitle>
|
</div>
|
||||||
<DialogDescription>
|
}
|
||||||
此操作将永久删除该团队,该操作无法撤销。
|
description="此操作将永久删除该团队,该操作无法撤销。"
|
||||||
</DialogDescription>
|
item={record}
|
||||||
</DialogHeader>
|
onConfirm={handleConfirm}
|
||||||
|
onSuccess={() => {
|
||||||
<div className="space-y-3 py-4">
|
toast({
|
||||||
|
title: '删除成功',
|
||||||
|
description: `团队 "${record.teamName}" 已删除`
|
||||||
|
});
|
||||||
|
onSuccess();
|
||||||
|
}}
|
||||||
|
variant="destructive"
|
||||||
|
confirmText="确认删除"
|
||||||
|
maxWidth="md"
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
<div className="rounded-lg bg-muted p-4 space-y-2">
|
<div className="rounded-lg bg-muted p-4 space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-muted-foreground">团队编码:</span>
|
<span className="text-sm text-muted-foreground">团队编码:</span>
|
||||||
@ -85,13 +75,17 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
|||||||
{record.memberCount !== undefined && record.memberCount > 0 && (
|
{record.memberCount !== undefined && record.memberCount > 0 && (
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-muted-foreground">成员数量:</span>
|
<span className="text-sm text-muted-foreground">成员数量:</span>
|
||||||
<span className="text-sm font-medium text-destructive">{record.memberCount} 人</span>
|
<span className="text-sm font-medium text-destructive">
|
||||||
|
{record.memberCount} 人
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{record.applicationCount !== undefined && record.applicationCount > 0 && (
|
{record.applicationCount !== undefined && record.applicationCount > 0 && (
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-muted-foreground">关联应用:</span>
|
<span className="text-sm text-muted-foreground">关联应用:</span>
|
||||||
<span className="text-sm font-medium text-destructive">{record.applicationCount} 个</span>
|
<span className="text-sm font-medium text-destructive">
|
||||||
|
{record.applicationCount} 个
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -106,19 +100,8 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</ConfirmDialog>
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button variant="destructive" onClick={handleConfirm}>
|
|
||||||
确认删除
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteDialog;
|
export default DeleteDialog;
|
||||||
|
|
||||||
|
|||||||
@ -5,16 +5,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@ -245,25 +236,7 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
|||||||
// 确认删除
|
// 确认删除
|
||||||
const handleDeleteConfirm = async () => {
|
const handleDeleteConfirm = async () => {
|
||||||
if (!deleteRecord) return;
|
if (!deleteRecord) return;
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteTeamMember(deleteRecord.id);
|
await deleteTeamMember(deleteRecord.id);
|
||||||
toast({
|
|
||||||
title: "删除成功",
|
|
||||||
description: `成员 "${deleteRecord.userName}" 已移除`,
|
|
||||||
});
|
|
||||||
setDeleteDialogOpen(false);
|
|
||||||
setDeleteRecord(null);
|
|
||||||
loadMembers();
|
|
||||||
onSuccess?.();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除失败:', error);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "删除失败",
|
|
||||||
description: error instanceof Error ? error.message : '未知错误',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 过滤出未加入的用户(排除负责人和已加入的成员)
|
// 过滤出未加入的用户(排除负责人和已加入的成员)
|
||||||
@ -488,24 +461,25 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* 删除确认对话框 */}
|
{/* 删除确认对话框 */}
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<ConfirmDialog
|
||||||
<AlertDialogContent>
|
open={deleteDialogOpen}
|
||||||
<AlertDialogHeader>
|
onOpenChange={setDeleteDialogOpen}
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
title="确认删除"
|
||||||
<AlertDialogDescription>
|
description={`确定要将成员 "${deleteRecord?.userName}" 从团队中移除吗?此操作无法撤销。`}
|
||||||
确定要将成员 "{deleteRecord?.userName}" 从团队中移除吗?此操作无法撤销。
|
item={deleteRecord}
|
||||||
</AlertDialogDescription>
|
onConfirm={handleDeleteConfirm}
|
||||||
</AlertDialogHeader>
|
onSuccess={() => {
|
||||||
<AlertDialogFooter>
|
toast({
|
||||||
<AlertDialogCancel onClick={() => setDeleteDialogOpen(false)}>
|
title: "删除成功",
|
||||||
取消
|
description: `成员 "${deleteRecord?.userName}" 已移除`,
|
||||||
</AlertDialogCancel>
|
});
|
||||||
<AlertDialogAction onClick={handleDeleteConfirm} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
setDeleteRecord(null);
|
||||||
确认删除
|
loadMembers();
|
||||||
</AlertDialogAction>
|
onSuccess?.();
|
||||||
</AlertDialogFooter>
|
}}
|
||||||
</AlertDialogContent>
|
variant="destructive"
|
||||||
</AlertDialog>
|
confirmText="确认删除"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -243,7 +243,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
|
|||||||
{/* 应用选择 */}
|
{/* 应用选择 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>
|
<Label>
|
||||||
应用名称 <span className="text-destructive">*</span>
|
应用 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.appId?.toString() || ''}
|
value={formData.appId?.toString() || ''}
|
||||||
@ -261,7 +261,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
applications.map((app) => (
|
applications.map((app) => (
|
||||||
<SelectItem key={app.id} value={app.id.toString()}>
|
<SelectItem key={app.id} value={app.id.toString()}>
|
||||||
{app.appName}
|
{app.appName}({app.appCode})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@ -477,7 +477,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
|
|||||||
|
|
||||||
{/* 工作流定义 */}
|
{/* 工作流定义 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>工作流定义</Label>
|
<Label>流程</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.workflowDefinitionId?.toString() || ''}
|
value={formData.workflowDefinitionId?.toString() || ''}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
|
|||||||
@ -15,16 +15,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import {
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '@/components/ui/alert-dialog';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { Plus, Edit, Trash2, Loader2 } from 'lucide-react';
|
import { Plus, Edit, Trash2, Loader2 } from 'lucide-react';
|
||||||
@ -76,7 +67,6 @@ export const TeamApplicationManageDialog: React.FC<
|
|||||||
// 删除确认对话框状态
|
// 删除确认对话框状态
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [deletingApp, setDeletingApp] = useState<TeamApplication | null>(null);
|
const [deletingApp, setDeletingApp] = useState<TeamApplication | null>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
|
||||||
|
|
||||||
// 加载基础数据和应用列表
|
// 加载基础数据和应用列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -196,27 +186,7 @@ export const TeamApplicationManageDialog: React.FC<
|
|||||||
|
|
||||||
const handleConfirmDelete = async () => {
|
const handleConfirmDelete = async () => {
|
||||||
if (!deletingApp) return;
|
if (!deletingApp) return;
|
||||||
|
|
||||||
setDeleting(true);
|
|
||||||
try {
|
|
||||||
await deleteTeamApplication(deletingApp.id!);
|
await deleteTeamApplication(deletingApp.id!);
|
||||||
toast({
|
|
||||||
title: '删除成功',
|
|
||||||
description: '应用配置已删除',
|
|
||||||
});
|
|
||||||
setDeleteDialogOpen(false);
|
|
||||||
setDeletingApp(null);
|
|
||||||
loadTeamApplications();
|
|
||||||
onSuccess?.();
|
|
||||||
} catch (error: any) {
|
|
||||||
toast({
|
|
||||||
title: '删除失败',
|
|
||||||
description: error.message || '删除应用配置失败',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setDeleting(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEnvironmentName = (environmentId: number) => {
|
const getEnvironmentName = (environmentId: number) => {
|
||||||
@ -346,29 +316,25 @@ export const TeamApplicationManageDialog: React.FC<
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 删除确认对话框 */}
|
{/* 删除确认对话框 */}
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<ConfirmDialog
|
||||||
<AlertDialogContent>
|
open={deleteDialogOpen}
|
||||||
<AlertDialogHeader>
|
onOpenChange={setDeleteDialogOpen}
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
title="确认删除"
|
||||||
<AlertDialogDescription>
|
description={`确定要删除应用配置 "${deletingApp?.applicationName || `应用 ${deletingApp?.applicationId}`}" 吗?此操作无法撤销。`}
|
||||||
确定要删除应用配置 "
|
item={deletingApp}
|
||||||
{deletingApp?.applicationName || `应用 ${deletingApp?.applicationId}`}" 吗?
|
onConfirm={handleConfirmDelete}
|
||||||
此操作无法撤销。
|
onSuccess={() => {
|
||||||
</AlertDialogDescription>
|
toast({
|
||||||
</AlertDialogHeader>
|
title: '删除成功',
|
||||||
<AlertDialogFooter>
|
description: '应用配置已删除',
|
||||||
<AlertDialogCancel disabled={deleting}>取消</AlertDialogCancel>
|
});
|
||||||
<AlertDialogAction
|
setDeletingApp(null);
|
||||||
onClick={handleConfirmDelete}
|
loadTeamApplications();
|
||||||
disabled={deleting}
|
onSuccess?.();
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
}}
|
||||||
>
|
variant="destructive"
|
||||||
{deleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
confirmText="删除"
|
||||||
删除
|
/>
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,16 +15,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import {
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '@/components/ui/alert-dialog';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
@ -85,7 +76,6 @@ export const TeamEnvironmentManageDialog: React.FC<
|
|||||||
const [deletingConfig, setDeletingConfig] = useState<
|
const [deletingConfig, setDeletingConfig] = useState<
|
||||||
TeamEnvironmentConfig | null
|
TeamEnvironmentConfig | null
|
||||||
>(null);
|
>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
|
||||||
|
|
||||||
// 加载团队的环境配置列表
|
// 加载团队的环境配置列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -139,28 +129,7 @@ export const TeamEnvironmentManageDialog: React.FC<
|
|||||||
|
|
||||||
const handleConfirmDelete = async () => {
|
const handleConfirmDelete = async () => {
|
||||||
if (!deletingConfig) return;
|
if (!deletingConfig) return;
|
||||||
|
|
||||||
setDeleting(true);
|
|
||||||
try {
|
|
||||||
await deleteTeamEnvironmentConfig(deletingConfig.id);
|
await deleteTeamEnvironmentConfig(deletingConfig.id);
|
||||||
|
|
||||||
toast({
|
|
||||||
title: '删除成功',
|
|
||||||
description: '环境配置已删除',
|
|
||||||
});
|
|
||||||
setDeleteDialogOpen(false);
|
|
||||||
setDeletingConfig(null);
|
|
||||||
loadEnvironmentConfigs();
|
|
||||||
onSuccess?.();
|
|
||||||
} catch (error: any) {
|
|
||||||
toast({
|
|
||||||
title: '删除失败',
|
|
||||||
description: error.message || '删除环境配置失败',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setDeleting(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfigSuccess = () => {
|
const handleConfigSuccess = () => {
|
||||||
@ -365,28 +334,25 @@ export const TeamEnvironmentManageDialog: React.FC<
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 删除确认对话框 */}
|
{/* 删除确认对话框 */}
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<ConfirmDialog
|
||||||
<AlertDialogContent>
|
open={deleteDialogOpen}
|
||||||
<AlertDialogHeader>
|
onOpenChange={setDeleteDialogOpen}
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
title="确认删除"
|
||||||
<AlertDialogDescription>
|
description={`确定要删除环境"${deletingConfig?.environmentName}"的配置吗?此操作将同时删除该环境下的所有应用配置,且无法撤销。`}
|
||||||
确定要删除环境"{deletingConfig?.environmentName}"的配置吗?
|
item={deletingConfig}
|
||||||
此操作将同时删除该环境下的所有应用配置,且无法撤销。
|
onConfirm={handleConfirmDelete}
|
||||||
</AlertDialogDescription>
|
onSuccess={() => {
|
||||||
</AlertDialogHeader>
|
toast({
|
||||||
<AlertDialogFooter>
|
title: '删除成功',
|
||||||
<AlertDialogCancel disabled={deleting}>取消</AlertDialogCancel>
|
description: '环境配置已删除',
|
||||||
<AlertDialogAction
|
});
|
||||||
onClick={handleConfirmDelete}
|
setDeletingConfig(null);
|
||||||
disabled={deleting}
|
loadEnvironmentConfigs();
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
onSuccess?.();
|
||||||
>
|
}}
|
||||||
{deleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
variant="destructive"
|
||||||
删除
|
confirmText="删除"
|
||||||
</AlertDialogAction>
|
/>
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -43,7 +43,7 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
User,
|
User,
|
||||||
GitCommit,
|
GitCommit,
|
||||||
Download,
|
RotateCw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type {
|
import type {
|
||||||
RepositoryGroupResponse,
|
RepositoryGroupResponse,
|
||||||
@ -111,6 +111,13 @@ const GitManager: React.FC = () => {
|
|||||||
branches: false,
|
branches: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 数量统计
|
||||||
|
const [projectCount, setProjectCount] = useState(0);
|
||||||
|
const [branchCount, setBranchCount] = useState(0);
|
||||||
|
|
||||||
|
// 刷新触发器
|
||||||
|
const [projectRefreshKey, setProjectRefreshKey] = useState(0);
|
||||||
|
const [branchRefreshKey, setBranchRefreshKey] = useState(0);
|
||||||
|
|
||||||
// 搜索过滤 - 仅保留仓库组搜索
|
// 搜索过滤 - 仅保留仓库组搜索
|
||||||
const [groupSearch, setGroupSearch] = useState('');
|
const [groupSearch, setGroupSearch] = useState('');
|
||||||
@ -193,9 +200,6 @@ const GitManager: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [selectedInstanceId, buildTree, toast]);
|
}, [selectedInstanceId, buildTree, toast]);
|
||||||
|
|
||||||
// 加载项目列表(支持不传repoGroupId加载所有项目)- 现在不再需要,由 InfiniteScrollList 处理
|
|
||||||
// 加载分支列表(支持不传repoProjectId加载该组所有分支)- 现在不再需要,由 InfiniteScrollList 处理
|
|
||||||
|
|
||||||
// 同步所有数据(异步任务)
|
// 同步所有数据(异步任务)
|
||||||
const handleSyncAll = useCallback(async () => {
|
const handleSyncAll = useCallback(async () => {
|
||||||
if (!selectedInstanceId) return;
|
if (!selectedInstanceId) return;
|
||||||
@ -462,8 +466,6 @@ const GitManager: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 项目和分支列表现在由 InfiniteScrollList 组件内部管理,不再需要客户端过滤
|
|
||||||
|
|
||||||
// 加载 Git 实例列表(只在组件挂载时执行一次)
|
// 加载 Git 实例列表(只在组件挂载时执行一次)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadInstancesEffect = async () => {
|
const loadInstancesEffect = async () => {
|
||||||
@ -513,6 +515,17 @@ const GitManager: React.FC = () => {
|
|||||||
|
|
||||||
const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, filterGroupTree]);
|
const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, filterGroupTree]);
|
||||||
|
|
||||||
|
// 缓存 resetTrigger 避免不必要的重置
|
||||||
|
const projectResetTrigger = React.useMemo(
|
||||||
|
() => [selectedInstanceId, selectedGroup?.id, projectRefreshKey],
|
||||||
|
[selectedInstanceId, selectedGroup?.id, projectRefreshKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
const branchResetTrigger = React.useMemo(
|
||||||
|
() => [selectedInstanceId, selectedProject?.id, branchRefreshKey],
|
||||||
|
[selectedInstanceId, selectedProject?.id, branchRefreshKey]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@ -576,6 +589,7 @@ const GitManager: React.FC = () => {
|
|||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={loadGroupTree}
|
onClick={loadGroupTree}
|
||||||
disabled={loading.groups || !selectedInstanceId}
|
disabled={loading.groups || !selectedInstanceId}
|
||||||
|
title="刷新列表"
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 ${loading.groups ? 'animate-spin' : ''}`}
|
className={`h-4 w-4 ${loading.groups ? 'animate-spin' : ''}`}
|
||||||
@ -587,9 +601,10 @@ const GitManager: React.FC = () => {
|
|||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={handleSyncGroups}
|
onClick={handleSyncGroups}
|
||||||
disabled={syncing.groups || !selectedInstanceId}
|
disabled={syncing.groups || !selectedInstanceId}
|
||||||
|
title="从Git同步"
|
||||||
>
|
>
|
||||||
<Download
|
<RotateCw
|
||||||
className={`h-4 w-4 ${syncing.groups ? 'animate-bounce' : ''}`}
|
className={`h-4 w-4 ${syncing.groups ? 'animate-spin' : ''}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -632,19 +647,30 @@ const GitManager: React.FC = () => {
|
|||||||
<Card className="col-span-5 flex flex-col min-h-0">
|
<Card className="col-span-5 flex flex-col min-h-0">
|
||||||
<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 flex items-center gap-2">
|
||||||
项目{selectedGroup && ` - ${selectedGroup.name}`}
|
项目 ({projectCount}){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={() => setProjectRefreshKey(prev => prev + 1)}
|
||||||
|
disabled={!selectedInstanceId}
|
||||||
|
title="刷新列表"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</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}
|
||||||
|
title="从Git同步"
|
||||||
>
|
>
|
||||||
<Download
|
<RotateCw
|
||||||
className={`h-4 w-4 ${syncing.projects ? 'animate-bounce' : ''}`}
|
className={`h-4 w-4 ${syncing.projects ? 'animate-spin' : ''}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -663,6 +689,7 @@ const GitManager: React.FC = () => {
|
|||||||
size,
|
size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
onDataChange={(data, total) => setProjectCount(data.length)}
|
||||||
renderItem={(project) => (
|
renderItem={(project) => (
|
||||||
<div
|
<div
|
||||||
key={project.id}
|
key={project.id}
|
||||||
@ -734,7 +761,7 @@ const GitManager: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
scrollableTarget="projectScrollableDiv"
|
scrollableTarget="projectScrollableDiv"
|
||||||
pageSize={20}
|
pageSize={20}
|
||||||
resetTrigger={selectedGroup?.id}
|
resetTrigger={projectResetTrigger}
|
||||||
emptyText={selectedGroup ? '当前仓库组暂无项目' : '暂无项目'}
|
emptyText={selectedGroup ? '当前仓库组暂无项目' : '暂无项目'}
|
||||||
className="p-2 space-y-2"
|
className="p-2 space-y-2"
|
||||||
/>
|
/>
|
||||||
@ -745,19 +772,30 @@ const GitManager: React.FC = () => {
|
|||||||
<Card className="col-span-4 flex flex-col min-h-0">
|
<Card className="col-span-4 flex flex-col min-h-0">
|
||||||
<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 flex items-center gap-2">
|
||||||
分支{selectedProject && ` - ${selectedProject.name}`}
|
分支 ({branchCount}){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={() => setBranchRefreshKey(prev => prev + 1)}
|
||||||
|
disabled={!selectedInstanceId}
|
||||||
|
title="刷新列表"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</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 || !selectedInstanceId}
|
disabled={syncing.branches || !selectedInstanceId}
|
||||||
|
title="从Git同步"
|
||||||
>
|
>
|
||||||
<Download
|
<RotateCw
|
||||||
className={`h-4 w-4 ${syncing.branches ? 'animate-bounce' : ''}`}
|
className={`h-4 w-4 ${syncing.branches ? 'animate-spin' : ''}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -776,6 +814,7 @@ const GitManager: React.FC = () => {
|
|||||||
size,
|
size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
onDataChange={(data, total) => setBranchCount(data.length)}
|
||||||
renderItem={(branch) => (
|
renderItem={(branch) => (
|
||||||
<div key={branch.id} className="p-3 hover:bg-accent transition-colors border-b">
|
<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">
|
||||||
@ -845,7 +884,7 @@ const GitManager: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
scrollableTarget="branchScrollableDiv"
|
scrollableTarget="branchScrollableDiv"
|
||||||
pageSize={20}
|
pageSize={20}
|
||||||
resetTrigger={selectedProject?.id}
|
resetTrigger={branchResetTrigger}
|
||||||
emptyText={selectedProject ? '当前项目暂无分支' : '暂无分支'}
|
emptyText={selectedProject ? '当前项目暂无分支' : '暂无分支'}
|
||||||
className=""
|
className=""
|
||||||
/>
|
/>
|
||||||
@ -859,4 +898,3 @@ const GitManager: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default GitManager;
|
export default GitManager;
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export const getRepositoryProjects = (params: {
|
|||||||
}) =>
|
}) =>
|
||||||
request.get<Page<RepositoryProjectResponse>>(`${PROJECT_URL}/page`, {
|
request.get<Page<RepositoryProjectResponse>>(`${PROJECT_URL}/page`, {
|
||||||
params: {
|
params: {
|
||||||
pageNum: params.pageNum ?? 0,
|
pageNum: params.pageNum ?? 1,
|
||||||
size: params.size ?? 20,
|
size: params.size ?? 20,
|
||||||
externalSystemId: params.externalSystemId,
|
externalSystemId: params.externalSystemId,
|
||||||
repoGroupId: params.repoGroupId,
|
repoGroupId: params.repoGroupId,
|
||||||
@ -84,7 +84,7 @@ export const getRepositoryBranches = (params: {
|
|||||||
}) =>
|
}) =>
|
||||||
request.get<Page<RepositoryBranchResponse>>(`${BRANCH_URL}/page`, {
|
request.get<Page<RepositoryBranchResponse>>(`${BRANCH_URL}/page`, {
|
||||||
params: {
|
params: {
|
||||||
pageNum: params.pageNum ?? 0,
|
pageNum: params.pageNum ?? 1,
|
||||||
size: params.size ?? 20,
|
size: params.size ?? 20,
|
||||||
sortField: 'lastUpdateTime',
|
sortField: 'lastUpdateTime',
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
import { InfiniteScrollList } from '@/components/ui/infinite-scroll-list';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@ -38,7 +38,7 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Calendar,
|
Calendar,
|
||||||
Activity,
|
Activity,
|
||||||
Download,
|
RotateCw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type {
|
import type {
|
||||||
JenkinsInstance,
|
JenkinsInstance,
|
||||||
@ -85,25 +85,13 @@ const JenkinsManager: React.FC = () => {
|
|||||||
|
|
||||||
// 数据状态
|
// 数据状态
|
||||||
const [views, setViews] = useState<JenkinsViewDTO[]>([]);
|
const [views, setViews] = useState<JenkinsViewDTO[]>([]);
|
||||||
const [jobs, setJobs] = useState<JenkinsJobDTO[]>([]);
|
|
||||||
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>();
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const [loading, setLoading] = useState({
|
const [loading, setLoading] = useState({ views: false });
|
||||||
views: false,
|
|
||||||
jobs: false,
|
|
||||||
builds: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 同步状态
|
// 同步状态
|
||||||
const [syncing, setSyncing] = useState({
|
const [syncing, setSyncing] = useState({
|
||||||
@ -117,39 +105,13 @@ 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 [jobCount, setJobCount] = useState(0);
|
||||||
const [debouncedJobSearch, setDebouncedJobSearch] = useState('');
|
const [buildCount, setBuildCount] = useState(0);
|
||||||
const [debouncedBuildSearch, setDebouncedBuildSearch] = useState('');
|
|
||||||
|
|
||||||
// 使用 ref 跟踪初始化状态,避免重复请求
|
// 刷新触发器
|
||||||
const isInitializedRef = useRef(false);
|
const [jobRefreshKey, setJobRefreshKey] = useState(0);
|
||||||
const viewsLoadedRef = useRef(false);
|
const [buildRefreshKey, setBuildRefreshKey] = useState(0);
|
||||||
const jobsLoadedRef = useRef(false);
|
|
||||||
|
|
||||||
// 视图搜索防抖
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDebouncedViewSearch(viewSearch);
|
|
||||||
}, 300);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [viewSearch]);
|
|
||||||
|
|
||||||
// 任务搜索防抖
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDebouncedJobSearch(jobSearch);
|
|
||||||
}, 300);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [jobSearch]);
|
|
||||||
|
|
||||||
// 构建搜索防抖(特别重要,因为有5000+条数据)
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDebouncedBuildSearch(buildSearch);
|
|
||||||
}, 300);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [buildSearch]);
|
|
||||||
|
|
||||||
// 加载视图列表
|
// 加载视图列表
|
||||||
const loadViews = useCallback(async () => {
|
const loadViews = useCallback(async () => {
|
||||||
@ -169,127 +131,7 @@ const JenkinsManager: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [selectedInstanceId, toast]);
|
}, [selectedInstanceId, toast]);
|
||||||
|
|
||||||
// 加载任务列表(首次加载或重置)
|
// 加载 Jenkins 实例列表
|
||||||
const loadJobs = useCallback(async (viewId?: number, reset: boolean = true) => {
|
|
||||||
if (!selectedInstanceId) return;
|
|
||||||
setLoading((prev) => ({ ...prev, jobs: true }));
|
|
||||||
try {
|
|
||||||
const pageNum = reset ? 0 : jobPage;
|
|
||||||
const response = await getJenkinsJobs({
|
|
||||||
externalSystemId: selectedInstanceId,
|
|
||||||
viewId,
|
|
||||||
pageNum,
|
|
||||||
size: 20,
|
|
||||||
});
|
|
||||||
if (response) {
|
|
||||||
if (reset) {
|
|
||||||
setJobs(response.content || []);
|
|
||||||
setJobPage(0);
|
|
||||||
} else {
|
|
||||||
setJobs((prev) => [...prev, ...(response.content || [])]);
|
|
||||||
}
|
|
||||||
setJobHasMore(!response.last);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
variant: 'destructive',
|
|
||||||
title: '加载失败',
|
|
||||||
description: '获取任务列表失败',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading((prev) => ({ ...prev, jobs: false }));
|
|
||||||
}
|
|
||||||
}, [selectedInstanceId, jobPage, toast]);
|
|
||||||
|
|
||||||
// 加载更多任务
|
|
||||||
const loadMoreJobs = useCallback(async () => {
|
|
||||||
if (!selectedInstanceId || !jobHasMore || loading.jobs) return;
|
|
||||||
setLoading((prev) => ({ ...prev, jobs: true }));
|
|
||||||
try {
|
|
||||||
const nextPage = jobPage + 1;
|
|
||||||
const response = await getJenkinsJobs({
|
|
||||||
externalSystemId: selectedInstanceId,
|
|
||||||
viewId: selectedView?.id,
|
|
||||||
pageNum: nextPage,
|
|
||||||
size: 20,
|
|
||||||
});
|
|
||||||
if (response) {
|
|
||||||
setJobs((prev) => [...prev, ...(response.content || [])]);
|
|
||||||
setJobPage(nextPage);
|
|
||||||
setJobHasMore(!response.last);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
variant: 'destructive',
|
|
||||||
title: '加载失败',
|
|
||||||
description: '获取更多任务失败',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading((prev) => ({ ...prev, jobs: false }));
|
|
||||||
}
|
|
||||||
}, [selectedInstanceId, selectedView, jobPage, jobHasMore, loading.jobs, toast]);
|
|
||||||
|
|
||||||
// 加载构建列表(首次加载或重置)
|
|
||||||
const loadBuilds = useCallback(async (jobId?: number, reset: boolean = true) => {
|
|
||||||
if (!selectedInstanceId) return;
|
|
||||||
setLoading((prev) => ({ ...prev, builds: true }));
|
|
||||||
try {
|
|
||||||
const pageNum = reset ? 0 : buildPage;
|
|
||||||
const response = await getJenkinsBuilds({
|
|
||||||
externalSystemId: selectedInstanceId,
|
|
||||||
jobId,
|
|
||||||
pageNum,
|
|
||||||
size: 20,
|
|
||||||
});
|
|
||||||
if (response) {
|
|
||||||
if (reset) {
|
|
||||||
setBuilds(response.content || []);
|
|
||||||
setBuildPage(0);
|
|
||||||
} else {
|
|
||||||
setBuilds((prev) => [...prev, ...(response.content || [])]);
|
|
||||||
}
|
|
||||||
setBuildHasMore(!response.last);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
variant: 'destructive',
|
|
||||||
title: '加载失败',
|
|
||||||
description: '获取构建列表失败',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading((prev) => ({ ...prev, builds: false }));
|
|
||||||
}
|
|
||||||
}, [selectedInstanceId, buildPage, toast]);
|
|
||||||
|
|
||||||
// 加载更多构建
|
|
||||||
const loadMoreBuilds = useCallback(async () => {
|
|
||||||
if (!selectedInstanceId || !buildHasMore || loading.builds) return;
|
|
||||||
setLoading((prev) => ({ ...prev, builds: true }));
|
|
||||||
try {
|
|
||||||
const nextPage = buildPage + 1;
|
|
||||||
const response = await getJenkinsBuilds({
|
|
||||||
externalSystemId: selectedInstanceId,
|
|
||||||
jobId: selectedJob?.id,
|
|
||||||
pageNum: nextPage,
|
|
||||||
size: 20,
|
|
||||||
});
|
|
||||||
if (response) {
|
|
||||||
setBuilds((prev) => [...prev, ...(response.content || [])]);
|
|
||||||
setBuildPage(nextPage);
|
|
||||||
setBuildHasMore(!response.last);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
variant: 'destructive',
|
|
||||||
title: '加载失败',
|
|
||||||
description: '获取更多构建失败',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading((prev) => ({ ...prev, builds: false }));
|
|
||||||
}
|
|
||||||
}, [selectedInstanceId, selectedJob, buildPage, buildHasMore, loading.builds, toast]);
|
|
||||||
|
|
||||||
// 加载 Jenkins 实例列表(只在组件挂载时执行一次)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadInstances = async () => {
|
const loadInstances = async () => {
|
||||||
try {
|
try {
|
||||||
@ -308,7 +150,16 @@ const JenkinsManager: React.FC = () => {
|
|||||||
};
|
};
|
||||||
loadInstances();
|
loadInstances();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []); // 只在组件挂载时执行一次
|
}, []);
|
||||||
|
|
||||||
|
// 实例变化时加载视图
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedInstanceId) return;
|
||||||
|
setSelectedView(undefined);
|
||||||
|
setSelectedJob(undefined);
|
||||||
|
loadViews();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedInstanceId]);
|
||||||
|
|
||||||
// 同步视图
|
// 同步视图
|
||||||
const handleSyncViews = useCallback(async () => {
|
const handleSyncViews = useCallback(async () => {
|
||||||
@ -345,10 +196,6 @@ const JenkinsManager: React.FC = () => {
|
|||||||
title: '同步成功',
|
title: '同步成功',
|
||||||
description: selectedView ? '视图任务同步任务已启动' : '所有任务同步任务已启动',
|
description: selectedView ? '视图任务同步任务已启动' : '所有任务同步任务已启动',
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
|
||||||
// 无论是否选择视图,都重新加载任务列表
|
|
||||||
loadJobs(selectedView?.id);
|
|
||||||
}, 2000);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
@ -358,7 +205,7 @@ const JenkinsManager: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setSyncing((prev) => ({ ...prev, jobs: false }));
|
setSyncing((prev) => ({ ...prev, jobs: false }));
|
||||||
}
|
}
|
||||||
}, [selectedInstanceId, selectedView, toast, loadJobs]);
|
}, [selectedInstanceId, selectedView, toast]);
|
||||||
|
|
||||||
// 同步构建
|
// 同步构建
|
||||||
const handleSyncBuilds = useCallback(async () => {
|
const handleSyncBuilds = useCallback(async () => {
|
||||||
@ -373,10 +220,6 @@ const JenkinsManager: React.FC = () => {
|
|||||||
title: '同步成功',
|
title: '同步成功',
|
||||||
description: selectedJob ? '任务构建同步任务已启动' : '所有构建同步任务已启动',
|
description: selectedJob ? '任务构建同步任务已启动' : '所有构建同步任务已启动',
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
|
||||||
// 无论是否选择任务,都重新加载构建列表
|
|
||||||
loadBuilds(selectedJob?.id);
|
|
||||||
}, 2000);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
@ -386,109 +229,15 @@ const JenkinsManager: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setSyncing((prev) => ({ ...prev, builds: false }));
|
setSyncing((prev) => ({ ...prev, builds: false }));
|
||||||
}
|
}
|
||||||
}, [selectedInstanceId, selectedJob, toast, loadBuilds]);
|
}, [selectedInstanceId, selectedJob, toast]);
|
||||||
|
|
||||||
// 实例变化时 - 重置状态并加载视图
|
// 过滤视图
|
||||||
useEffect(() => {
|
const filteredViews = React.useMemo(() => {
|
||||||
if (!selectedInstanceId) return;
|
if (!viewSearch) return views;
|
||||||
|
|
||||||
// 重置所有状态和标志
|
|
||||||
isInitializedRef.current = false;
|
|
||||||
viewsLoadedRef.current = false;
|
|
||||||
jobsLoadedRef.current = false;
|
|
||||||
|
|
||||||
setSelectedView(undefined);
|
|
||||||
setSelectedJob(undefined);
|
|
||||||
setJobs([]);
|
|
||||||
setBuilds([]);
|
|
||||||
setJobPage(0);
|
|
||||||
setJobHasMore(true);
|
|
||||||
setBuildPage(0);
|
|
||||||
setBuildHasMore(true);
|
|
||||||
|
|
||||||
// 加载视图列表
|
|
||||||
loadViews();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selectedInstanceId]); // 只监听 selectedInstanceId,避免重复请求
|
|
||||||
|
|
||||||
// 视图加载完成后 - 自动加载任务(只执行一次)
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedInstanceId && views.length > 0 && !viewsLoadedRef.current) {
|
|
||||||
viewsLoadedRef.current = true;
|
|
||||||
loadJobs(undefined, true);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selectedInstanceId, views.length]); // 监听 views.length 变化
|
|
||||||
|
|
||||||
// 任务加载完成后 - 自动加载构建(只执行一次)
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedInstanceId && jobs.length > 0 && !jobsLoadedRef.current) {
|
|
||||||
jobsLoadedRef.current = true;
|
|
||||||
isInitializedRef.current = true; // 标记初始化完成
|
|
||||||
loadBuilds(undefined, true);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selectedInstanceId, jobs.length]); // 监听 jobs.length 变化
|
|
||||||
|
|
||||||
// 视图选择变化 - 重新加载任务
|
|
||||||
useEffect(() => {
|
|
||||||
// 只有初始化完成后才处理选择变化
|
|
||||||
if (!isInitializedRef.current) return;
|
|
||||||
|
|
||||||
// 立即清空旧数据,避免渲染大量过时数据导致卡顿
|
|
||||||
setSelectedJob(undefined);
|
|
||||||
setJobs([]); // 🔥 关键:立即清空任务数据
|
|
||||||
setBuilds([]);
|
|
||||||
setJobPage(0);
|
|
||||||
setJobHasMore(true);
|
|
||||||
setBuildPage(0);
|
|
||||||
setBuildHasMore(true);
|
|
||||||
|
|
||||||
// 重新加载任务
|
|
||||||
loadJobs(selectedView?.id, true);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selectedView?.id]); // 只监听 selectedView?.id
|
|
||||||
|
|
||||||
// 任务选择变化 - 重新加载构建
|
|
||||||
useEffect(() => {
|
|
||||||
// 只有初始化完成后才处理选择变化
|
|
||||||
if (!isInitializedRef.current) return;
|
|
||||||
|
|
||||||
// 立即清空旧数据,避免渲染大量过时数据导致卡顿
|
|
||||||
setBuilds([]); // 🔥 关键:立即清空构建数据
|
|
||||||
setBuildPage(0);
|
|
||||||
setBuildHasMore(true);
|
|
||||||
|
|
||||||
// 重新加载构建
|
|
||||||
loadBuilds(selectedJob?.id, true);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selectedJob?.id]); // 只监听 selectedJob?.id
|
|
||||||
|
|
||||||
// 过滤视图(使用防抖搜索值)
|
|
||||||
const filteredViews = useMemo(() => {
|
|
||||||
if (!debouncedViewSearch) return views;
|
|
||||||
return views.filter((view) =>
|
return views.filter((view) =>
|
||||||
view.viewName.toLowerCase().includes(debouncedViewSearch.toLowerCase())
|
view.viewName.toLowerCase().includes(viewSearch.toLowerCase())
|
||||||
);
|
);
|
||||||
}, [views, debouncedViewSearch]);
|
}, [views, viewSearch]);
|
||||||
|
|
||||||
// 过滤任务(使用防抖搜索值)
|
|
||||||
const filteredJobs = useMemo(() => {
|
|
||||||
if (!debouncedJobSearch) return jobs;
|
|
||||||
return jobs.filter((job) =>
|
|
||||||
job.jobName.toLowerCase().includes(debouncedJobSearch.toLowerCase())
|
|
||||||
);
|
|
||||||
}, [jobs, debouncedJobSearch]);
|
|
||||||
|
|
||||||
// 过滤构建(使用防抖搜索值 - 特别重要,5000+条数据)
|
|
||||||
const filteredBuilds = useMemo(() => {
|
|
||||||
if (!debouncedBuildSearch) return builds;
|
|
||||||
const searchLower = debouncedBuildSearch.toLowerCase();
|
|
||||||
return builds.filter((build) =>
|
|
||||||
build.buildNumber.toString().includes(searchLower) ||
|
|
||||||
build.buildStatus.toLowerCase().includes(searchLower)
|
|
||||||
);
|
|
||||||
}, [builds, debouncedBuildSearch]);
|
|
||||||
|
|
||||||
// 获取构建状态徽章
|
// 获取构建状态徽章
|
||||||
const getBuildStatusBadge = useCallback((status: string) => {
|
const getBuildStatusBadge = useCallback((status: string) => {
|
||||||
@ -513,6 +262,16 @@ const JenkinsManager: React.FC = () => {
|
|||||||
return `${hours}小时${minutes % 60}分钟`;
|
return `${hours}小时${minutes % 60}分钟`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 缓存 resetTrigger 避免不必要的重置
|
||||||
|
const jobResetTrigger = React.useMemo(
|
||||||
|
() => [selectedInstanceId, selectedView?.id, jobRefreshKey],
|
||||||
|
[selectedInstanceId, selectedView?.id, jobRefreshKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildResetTrigger = React.useMemo(
|
||||||
|
() => [selectedInstanceId, selectedJob?.id, buildRefreshKey],
|
||||||
|
[selectedInstanceId, selectedJob?.id, buildRefreshKey]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@ -559,10 +318,9 @@ const JenkinsManager: React.FC = () => {
|
|||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={loadViews}
|
onClick={loadViews}
|
||||||
disabled={loading.views || !selectedInstanceId}
|
disabled={loading.views || !selectedInstanceId}
|
||||||
|
title="刷新列表"
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw className={`h-4 w-4 ${loading.views ? 'animate-spin' : ''}`} />
|
||||||
className={`h-4 w-4 ${loading.views ? 'animate-spin' : ''}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -570,10 +328,9 @@ const JenkinsManager: React.FC = () => {
|
|||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={handleSyncViews}
|
onClick={handleSyncViews}
|
||||||
disabled={syncing.views || !selectedInstanceId}
|
disabled={syncing.views || !selectedInstanceId}
|
||||||
|
title="从Jenkins同步"
|
||||||
>
|
>
|
||||||
<Download
|
<RotateCw className={`h-4 w-4 ${syncing.views ? 'animate-spin' : ''}`} />
|
||||||
className={`h-4 w-4 ${syncing.views ? 'animate-bounce' : ''}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -603,16 +360,12 @@ const JenkinsManager: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
key={view.id}
|
key={view.id}
|
||||||
className={`group p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${
|
className={`group p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${
|
||||||
selectedView?.id === view.id
|
selectedView?.id === view.id ? 'bg-accent border-primary' : ''
|
||||||
? 'bg-accent border-primary'
|
|
||||||
: ''
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setSelectedView(selectedView?.id === view.id ? undefined : 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">{view.viewName}</span>
|
||||||
{view.viewName}
|
|
||||||
</span>
|
|
||||||
{view.jobCount !== undefined && view.jobCount > 0 && (
|
{view.jobCount !== undefined && view.jobCount > 0 && (
|
||||||
<Badge variant="secondary" className="text-xs px-1.5 py-0 h-5">
|
<Badge variant="secondary" className="text-xs px-1.5 py-0 h-5">
|
||||||
{view.jobCount}
|
{view.jobCount}
|
||||||
@ -655,20 +408,19 @@ const JenkinsManager: React.FC = () => {
|
|||||||
<Card className="col-span-5 flex flex-col min-h-0">
|
<Card className="col-span-5 flex flex-col min-h-0">
|
||||||
<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 flex items-center gap-2">
|
||||||
任务 ({filteredJobs.length}){selectedView && ` - ${selectedView.viewName}`}
|
任务 ({jobCount}){selectedView && ` - ${selectedView.viewName}`}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={() => loadJobs(selectedView?.id)}
|
onClick={() => setJobRefreshKey(prev => prev + 1)}
|
||||||
disabled={loading.jobs || !selectedInstanceId}
|
disabled={!selectedInstanceId}
|
||||||
|
title="刷新列表"
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw className="h-4 w-4" />
|
||||||
className={`h-4 w-4 ${loading.jobs ? 'animate-spin' : ''}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -676,10 +428,9 @@ const JenkinsManager: React.FC = () => {
|
|||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={handleSyncJobs}
|
onClick={handleSyncJobs}
|
||||||
disabled={syncing.jobs || !selectedInstanceId}
|
disabled={syncing.jobs || !selectedInstanceId}
|
||||||
|
title="从Jenkins同步"
|
||||||
>
|
>
|
||||||
<Download
|
<RotateCw className={`h-4 w-4 ${syncing.jobs ? 'animate-spin' : ''}`} />
|
||||||
className={`h-4 w-4 ${syncing.jobs ? 'animate-bounce' : ''}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -693,48 +444,19 @@ const JenkinsManager: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<div
|
<div id="jobScrollableDiv" className="flex-1 overflow-auto">
|
||||||
id="jobScrollableDiv"
|
<InfiniteScrollList<JenkinsJobDTO>
|
||||||
className="flex-1 overflow-auto"
|
loadData={(pageNum, size) => getJenkinsJobs({
|
||||||
>
|
externalSystemId: selectedInstanceId!,
|
||||||
{loading.jobs && filteredJobs.length === 0 ? (
|
viewId: selectedView?.id,
|
||||||
<div className="flex items-center justify-center h-full">
|
pageNum,
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
size,
|
||||||
</div>
|
})}
|
||||||
) : filteredJobs.length === 0 ? (
|
onDataChange={(data, total) => setJobCount(data.length)}
|
||||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
renderItem={(job, index, isSelected) => (
|
||||||
<Box className="h-12 w-12 mb-2 opacity-20" />
|
<div className={`group p-3 border rounded-lg transition-colors ${
|
||||||
<p className="text-sm">{selectedView ? '当前视图暂无任务' : '暂无任务'}</p>
|
isSelected ? 'bg-accent border-primary' : 'hover:bg-accent'
|
||||||
</div>
|
}`}>
|
||||||
) : (
|
|
||||||
<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) => (
|
|
||||||
<div
|
|
||||||
key={job.id}
|
|
||||||
className={`group p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${
|
|
||||||
selectedJob?.id === job.id
|
|
||||||
? 'bg-accent border-primary'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
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>
|
||||||
{job.buildCount !== undefined && job.buildCount > 0 && (
|
{job.buildCount !== undefined && job.buildCount > 0 && (
|
||||||
@ -763,13 +485,11 @@ const JenkinsManager: React.FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{job.description && (
|
{job.description && (
|
||||||
<p className="text-xs text-muted-foreground line-clamp-2 mb-2">
|
<p className="text-xs text-muted-foreground line-clamp-2 mb-2">
|
||||||
{job.description}
|
{job.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-3 text-xs">
|
<div className="flex items-center gap-3 text-xs">
|
||||||
{job.lastBuildStatus && getBuildStatusBadge(job.lastBuildStatus)}
|
{job.lastBuildStatus && getBuildStatusBadge(job.lastBuildStatus)}
|
||||||
{job.healthReportScore !== undefined && (
|
{job.healthReportScore !== undefined && (
|
||||||
@ -777,10 +497,7 @@ const JenkinsManager: React.FC = () => {
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Activity className="h-3 w-3 text-muted-foreground" />
|
<Activity className="h-3 w-3 text-muted-foreground" />
|
||||||
<Progress
|
<Progress value={job.healthReportScore} className="w-16 h-2" />
|
||||||
value={job.healthReportScore}
|
|
||||||
className="w-16 h-2"
|
|
||||||
/>
|
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{job.healthReportScore}%
|
{job.healthReportScore}%
|
||||||
</span>
|
</span>
|
||||||
@ -792,7 +509,6 @@ const JenkinsManager: React.FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{job.lastBuildTime && (
|
{job.lastBuildTime && (
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-2">
|
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-2">
|
||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
@ -800,9 +516,19 @@ const JenkinsManager: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</InfiniteScroll>
|
|
||||||
)}
|
)}
|
||||||
|
searchValue={jobSearch}
|
||||||
|
localFilter={(job, search) =>
|
||||||
|
job.jobName.toLowerCase().includes(search.toLowerCase())
|
||||||
|
}
|
||||||
|
selectedItem={selectedJob}
|
||||||
|
onItemClick={(job) => setSelectedJob(selectedJob?.id === job.id ? undefined : job)}
|
||||||
|
itemKey="id"
|
||||||
|
scrollableTarget="jobScrollableDiv"
|
||||||
|
emptyText={selectedView ? '当前视图暂无任务' : '暂无任务'}
|
||||||
|
emptyIcon={<Box className="h-12 w-12 mb-2 opacity-20" />}
|
||||||
|
resetTrigger={jobResetTrigger}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -810,20 +536,19 @@ const JenkinsManager: React.FC = () => {
|
|||||||
<Card className="col-span-4 flex flex-col min-h-0">
|
<Card className="col-span-4 flex flex-col min-h-0">
|
||||||
<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 flex items-center gap-2">
|
||||||
构建 ({filteredBuilds.length}){selectedJob && ` - ${selectedJob.jobName}`}
|
构建 ({buildCount}){selectedJob && ` - ${selectedJob.jobName}`}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={() => loadBuilds(selectedJob?.id)}
|
onClick={() => setBuildRefreshKey(prev => prev + 1)}
|
||||||
disabled={loading.builds || !selectedInstanceId}
|
disabled={!selectedInstanceId}
|
||||||
|
title="刷新列表"
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw className="h-4 w-4" />
|
||||||
className={`h-4 w-4 ${loading.builds ? 'animate-spin' : ''}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -831,10 +556,9 @@ const JenkinsManager: React.FC = () => {
|
|||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={handleSyncBuilds}
|
onClick={handleSyncBuilds}
|
||||||
disabled={syncing.builds || !selectedInstanceId}
|
disabled={syncing.builds || !selectedInstanceId}
|
||||||
|
title="从Jenkins同步"
|
||||||
>
|
>
|
||||||
<Download
|
<RotateCw className={`h-4 w-4 ${syncing.builds ? 'animate-spin' : ''}`} />
|
||||||
className={`h-4 w-4 ${syncing.builds ? 'animate-bounce' : ''}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -848,43 +572,17 @@ const JenkinsManager: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<div
|
<div id="buildScrollableDiv" className="flex-1 overflow-auto">
|
||||||
id="buildScrollableDiv"
|
<InfiniteScrollList<JenkinsBuildDTO>
|
||||||
className="flex-1 overflow-auto"
|
loadData={(pageNum, size) => getJenkinsBuilds({
|
||||||
>
|
externalSystemId: selectedInstanceId!,
|
||||||
{loading.builds && filteredBuilds.length === 0 ? (
|
jobId: selectedJob?.id,
|
||||||
<div className="flex items-center justify-center h-full">
|
pageNum,
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
size,
|
||||||
</div>
|
})}
|
||||||
) : filteredBuilds.length === 0 ? (
|
onDataChange={(data, total) => setBuildCount(data.length)}
|
||||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
renderItem={(build) => (
|
||||||
<GitBranch className="h-12 w-12 mb-2 opacity-20" />
|
<div className="group p-3 hover:bg-accent transition-colors border rounded-lg">
|
||||||
<p className="text-sm">{selectedJob ? '当前任务暂无构建' : '暂无构建'}</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<InfiniteScroll
|
|
||||||
dataLength={filteredBuilds.length}
|
|
||||||
next={loadMoreBuilds}
|
|
||||||
hasMore={buildHasMore}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
{filteredBuilds.map((build) => (
|
|
||||||
<div
|
|
||||||
key={build.id}
|
|
||||||
className="group p-3 hover:bg-accent transition-colors border rounded-lg"
|
|
||||||
>
|
|
||||||
<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">
|
||||||
<span className="font-mono font-semibold text-sm">
|
<span className="font-mono font-semibold text-sm">
|
||||||
@ -912,14 +610,12 @@ const JenkinsManager: React.FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{build.starttime && (
|
{build.starttime && (
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
|
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
|
||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
{dayjs(build.starttime).format('YYYY-MM-DD HH:mm:ss')}
|
{dayjs(build.starttime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{build.duration !== undefined && (
|
{build.duration !== undefined && (
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
|
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
@ -927,9 +623,18 @@ const JenkinsManager: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</InfiniteScroll>
|
|
||||||
)}
|
)}
|
||||||
|
searchValue={buildSearch}
|
||||||
|
localFilter={(build, search) => {
|
||||||
|
const searchLower = search.toLowerCase();
|
||||||
|
return build.buildNumber.toString().includes(searchLower) ||
|
||||||
|
build.buildStatus.toLowerCase().includes(searchLower);
|
||||||
|
}}
|
||||||
|
scrollableTarget="buildScrollableDiv"
|
||||||
|
emptyText={selectedJob ? '当前任务暂无构建' : '暂无构建'}
|
||||||
|
emptyIcon={<GitBranch className="h-12 w-12 mb-2 opacity-20" />}
|
||||||
|
resetTrigger={buildResetTrigger}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export const getJenkinsInstances = () =>
|
|||||||
|
|
||||||
// 获取视图列表
|
// 获取视图列表
|
||||||
export const getJenkinsViews = (externalSystemId: number) =>
|
export const getJenkinsViews = (externalSystemId: number) =>
|
||||||
request.get<JenkinsViewDTO[]>(`/api/v1/jenkins-view`, {
|
request.get<JenkinsViewDTO[]>(`/api/v1/jenkins-view/list`, {
|
||||||
params: { externalSystemId }
|
params: { externalSystemId }
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -28,6 +28,7 @@ export const syncJenkinsViews = (externalSystemId: number) =>
|
|||||||
// ==================== Jenkins 任务 ====================
|
// ==================== Jenkins 任务 ====================
|
||||||
|
|
||||||
// 获取任务列表(分页)
|
// 获取任务列表(分页)
|
||||||
|
// 注意:pageNum 从 1 开始,后端会自动转换为从 0 开始给 Spring Data JPA
|
||||||
export const getJenkinsJobs = (params: {
|
export const getJenkinsJobs = (params: {
|
||||||
externalSystemId: number;
|
externalSystemId: number;
|
||||||
viewId?: number;
|
viewId?: number;
|
||||||
@ -36,7 +37,7 @@ export const getJenkinsJobs = (params: {
|
|||||||
}) =>
|
}) =>
|
||||||
request.get<Page<JenkinsJobDTO>>(`/api/v1/jenkins-job/page`, {
|
request.get<Page<JenkinsJobDTO>>(`/api/v1/jenkins-job/page`, {
|
||||||
params: {
|
params: {
|
||||||
pageNum: params.pageNum ?? 0,
|
pageNum: params.pageNum ?? 1,
|
||||||
size: params.size ?? 20,
|
size: params.size ?? 20,
|
||||||
externalSystemId: params.externalSystemId,
|
externalSystemId: params.externalSystemId,
|
||||||
viewId: params.viewId
|
viewId: params.viewId
|
||||||
@ -50,6 +51,7 @@ export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: num
|
|||||||
// ==================== Jenkins 构建 ====================
|
// ==================== Jenkins 构建 ====================
|
||||||
|
|
||||||
// 获取构建列表(分页,按开始时间倒序排序)
|
// 获取构建列表(分页,按开始时间倒序排序)
|
||||||
|
// 注意:pageNum 从 1 开始,后端会自动转换为从 0 开始给 Spring Data JPA
|
||||||
export const getJenkinsBuilds = (params: {
|
export const getJenkinsBuilds = (params: {
|
||||||
externalSystemId: number;
|
externalSystemId: number;
|
||||||
jobId?: number;
|
jobId?: number;
|
||||||
@ -58,7 +60,7 @@ export const getJenkinsBuilds = (params: {
|
|||||||
}) =>
|
}) =>
|
||||||
request.get<Page<JenkinsBuildDTO>>(`/api/v1/jenkins-build/page`, {
|
request.get<Page<JenkinsBuildDTO>>(`/api/v1/jenkins-build/page`, {
|
||||||
params: {
|
params: {
|
||||||
pageNum: params.pageNum ?? 0,
|
pageNum: params.pageNum ?? 1,
|
||||||
size: params.size ?? 20,
|
size: params.size ?? 20,
|
||||||
sortField: 'starttime',
|
sortField: 'starttime',
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user