重构前端逻辑

This commit is contained in:
dengqichen 2025-11-07 11:22:26 +08:00
parent d9807a1816
commit 011e13dbfa
12 changed files with 909 additions and 1020 deletions

View 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>
);
}

View File

@ -6,7 +6,7 @@ import type { Page } from '@/types/base';
interface InfiniteScrollListProps<T> {
/**
*
* @param pageNum - 0
* @param pageNum - 1
* @param size -
* @param extraParams -
* @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
@ -53,16 +56,6 @@ interface InfiniteScrollListProps<T> {
*/
emptyIcon?: React.ReactNode;
/**
*
*/
emptyState?: React.ReactNode;
/**
*
*/
loadingPlaceholder?: React.ReactNode;
/**
*
*/
@ -82,6 +75,33 @@ interface InfiniteScrollListProps<T> {
*
*/
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>({
@ -98,16 +118,32 @@ export function InfiniteScrollList<T>({
showInitialLoading = true,
resetTrigger,
onDataChange,
searchValue = '',
localFilter,
selectedItem,
onItemClick,
itemKey,
}: InfiniteScrollListProps<T>) {
const [data, setData] = useState<T[]>([]);
const [page, setPage] = useState(0);
const [page, setPage] = useState(1); // ✅ 从 1 开始
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(showInitialLoading);
// ✅ 搜索防抖300ms
const [debouncedSearchValue, setDebouncedSearchValue] = useState(searchValue);
// 使用 ref 跟踪是否正在加载,避免重复请求
const loadingRef = useRef(false);
// ✅ 搜索防抖处理
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchValue(searchValue);
}, 300);
return () => clearTimeout(timer);
}, [searchValue]);
// 加载数据
const fetchData = useCallback(async (pageNum: number, reset: boolean = false) => {
if (loadingRef.current) return;
@ -155,17 +191,29 @@ export function InfiniteScrollList<T>({
// 重置并重新加载
const reset = useCallback(() => {
setData([]);
setPage(0);
setPage(1); // ✅ 重置为 1
setHasMore(true);
setInitialLoading(showInitialLoading);
fetchData(0, true);
fetchData(1, true); // ✅ 从第 1 页开始
}, [fetchData, showInitialLoading]);
// 初始加载
// 当 resetTrigger 或 debouncedSearchValue 变化时重置
useEffect(() => {
reset();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resetTrigger]);
}, [resetTrigger, debouncedSearchValue]);
// ✅ 本地过滤数据
const filteredData = React.useMemo(() => {
if (!localFilter || !debouncedSearchValue) return data;
return data.filter(item => localFilter(item, debouncedSearchValue));
}, [data, localFilter, debouncedSearchValue]);
// ✅ 判断项是否被选中
const isSelected = (item: T) => {
if (!selectedItem || !itemKey) return false;
return selectedItem[itemKey] === item[itemKey];
};
// 初始 loading 状态
if (initialLoading && data.length === 0) {
@ -177,7 +225,7 @@ export function InfiniteScrollList<T>({
}
// 空状态
if (!loading && data.length === 0) {
if (!loading && filteredData.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" />}
@ -189,7 +237,7 @@ export function InfiniteScrollList<T>({
// 列表渲染
return (
<InfiniteScroll
dataLength={data.length}
dataLength={filteredData.length}
next={loadMore}
hasMore={hasMore}
loader={
@ -199,7 +247,7 @@ export function InfiniteScrollList<T>({
</div>
}
endMessage={
data.length > 0 ? (
filteredData.length > 0 ? (
<div className="flex items-center justify-center py-4 text-sm text-muted-foreground">
{endMessage}
</div>
@ -208,15 +256,21 @@ export function InfiniteScrollList<T>({
scrollableTarget={scrollableTarget}
className={className || listClassName}
>
{data.map((item, index) => (
<React.Fragment key={index}>
{renderItem(item, index)}
</React.Fragment>
))}
{filteredData.map((item, index) => {
const selected = isSelected(item);
return (
<div
key={itemKey ? String(item[itemKey]) : index}
onClick={() => onItemClick?.(item)}
className={onItemClick ? 'cursor-pointer' : ''}
>
{renderItem(item, index, selected)}
</div>
);
})}
</InfiniteScroll>
);
}
// 导出类型供外部使用
export type { InfiniteScrollListProps };

View File

@ -5,16 +5,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@ -197,25 +188,7 @@ const ApplicationManageDialog: React.FC<ApplicationManageDialogProps> = ({
// 确认删除
const handleDeleteConfirm = async () => {
if (!deleteRecord) return;
try {
await deleteTeamApplication(deleteRecord.id);
toast({
title: "删除成功",
description: `应用 "${deleteRecord.applicationName}" 已移除`,
});
setDeleteDialogOpen(false);
setDeleteRecord(null);
loadApplications();
onSuccess?.();
} catch (error) {
console.error('删除失败:', error);
toast({
variant: "destructive",
title: "删除失败",
description: error instanceof Error ? error.message : '未知错误',
});
}
await deleteTeamApplication(deleteRecord.id);
};
// 过滤出未关联的应用
@ -401,24 +374,25 @@ const ApplicationManageDialog: React.FC<ApplicationManageDialogProps> = ({
</Dialog>
{/* 删除确认对话框 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"{deleteRecord?.applicationName}"
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setDeleteDialogOpen(false)}>
</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteConfirm} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
title="确认删除"
description={`确定要将应用 "${deleteRecord?.applicationName}" 从团队中移除吗?此操作无法撤销。`}
item={deleteRecord}
onConfirm={handleDeleteConfirm}
onSuccess={() => {
toast({
title: "删除成功",
description: `应用 "${deleteRecord?.applicationName}" 已移除`,
});
setDeleteRecord(null);
loadApplications();
onSuccess?.();
}}
variant="destructive"
confirmText="确认删除"
/>
</>
);
};

View File

@ -1,15 +1,7 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { AlertCircle } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { deleteTeam } from '../service';
import type { TeamResponse } from '../types';
@ -33,92 +25,83 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
const handleConfirm = async () => {
if (!record) return;
try {
await deleteTeam(record.id);
toast({ title: '删除成功', description: `团队 "${record.teamName}" 已删除` });
onSuccess();
onOpenChange(false);
} catch (error) {
console.error('删除失败:', error);
toast({
title: '删除失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
await deleteTeam(record.id);
};
if (!record) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-destructive" />
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-3 py-4">
<div className="rounded-lg bg-muted p-4 space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"></span>
<code className="text-sm font-mono bg-background px-2 py-1 rounded">
{record.teamCode}
</code>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm font-medium">{record.teamName}</span>
</div>
{record.ownerName && (
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{record.ownerName}</span>
</div>
)}
{record.memberCount !== undefined && record.memberCount > 0 && (
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm font-medium text-destructive">{record.memberCount} </span>
</div>
)}
{record.applicationCount !== undefined && record.applicationCount > 0 && (
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm font-medium text-destructive">{record.applicationCount} </span>
</div>
)}
<ConfirmDialog
open={open}
onOpenChange={onOpenChange}
title={
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-destructive" />
</div>
}
description="此操作将永久删除该团队,该操作无法撤销。"
item={record}
onConfirm={handleConfirm}
onSuccess={() => {
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="flex justify-between items-center">
<span className="text-sm text-muted-foreground"></span>
<code className="text-sm font-mono bg-background px-2 py-1 rounded">
{record.teamCode}
</code>
</div>
{((record.memberCount !== undefined && record.memberCount > 0) ||
(record.applicationCount !== undefined && record.applicationCount > 0)) && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-destructive/10 text-destructive">
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
<p className="text-sm">
{record.memberCount || 0}{record.applicationCount || 0}
</p>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm font-medium">{record.teamName}</span>
</div>
{record.ownerName && (
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{record.ownerName}</span>
</div>
)}
{record.memberCount !== undefined && record.memberCount > 0 && (
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm font-medium text-destructive">
{record.memberCount}
</span>
</div>
)}
{record.applicationCount !== undefined && record.applicationCount > 0 && (
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm font-medium text-destructive">
{record.applicationCount}
</span>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button variant="destructive" onClick={handleConfirm}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{((record.memberCount !== undefined && record.memberCount > 0) ||
(record.applicationCount !== undefined && record.applicationCount > 0)) && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-destructive/10 text-destructive">
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
<p className="text-sm">
{record.memberCount || 0}{record.applicationCount || 0}
</p>
</div>
)}
</div>
</ConfirmDialog>
);
};
export default DeleteDialog;

View File

@ -5,16 +5,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@ -245,25 +236,7 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
// 确认删除
const handleDeleteConfirm = async () => {
if (!deleteRecord) return;
try {
await deleteTeamMember(deleteRecord.id);
toast({
title: "删除成功",
description: `成员 "${deleteRecord.userName}" 已移除`,
});
setDeleteDialogOpen(false);
setDeleteRecord(null);
loadMembers();
onSuccess?.();
} catch (error) {
console.error('删除失败:', error);
toast({
variant: "destructive",
title: "删除失败",
description: error instanceof Error ? error.message : '未知错误',
});
}
await deleteTeamMember(deleteRecord.id);
};
// 过滤出未加入的用户(排除负责人和已加入的成员)
@ -488,24 +461,25 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
</Dialog>
{/* 删除确认对话框 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"{deleteRecord?.userName}"
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setDeleteDialogOpen(false)}>
</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteConfirm} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
title="确认删除"
description={`确定要将成员 "${deleteRecord?.userName}" 从团队中移除吗?此操作无法撤销。`}
item={deleteRecord}
onConfirm={handleDeleteConfirm}
onSuccess={() => {
toast({
title: "删除成功",
description: `成员 "${deleteRecord?.userName}" 已移除`,
});
setDeleteRecord(null);
loadMembers();
onSuccess?.();
}}
variant="destructive"
confirmText="确认删除"
/>
</>
);
};

View File

@ -243,7 +243,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
{/* 应用选择 */}
<div className="space-y-2">
<Label>
<span className="text-destructive">*</span>
<span className="text-destructive">*</span>
</Label>
<Select
value={formData.appId?.toString() || ''}
@ -261,7 +261,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
) : (
applications.map((app) => (
<SelectItem key={app.id} value={app.id.toString()}>
{app.appName}
{app.appName}{app.appCode}
</SelectItem>
))
)}
@ -477,7 +477,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
{/* 工作流定义 */}
<div className="space-y-2">
<Label></Label>
<Label></Label>
<Select
value={formData.workflowDefinitionId?.toString() || ''}
onValueChange={(value) =>

View File

@ -15,16 +15,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/use-toast';
import { Plus, Edit, Trash2, Loader2 } from 'lucide-react';
@ -76,7 +67,6 @@ export const TeamApplicationManageDialog: React.FC<
// 删除确认对话框状态
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deletingApp, setDeletingApp] = useState<TeamApplication | null>(null);
const [deleting, setDeleting] = useState(false);
// 加载基础数据和应用列表
useEffect(() => {
@ -196,27 +186,7 @@ export const TeamApplicationManageDialog: React.FC<
const handleConfirmDelete = async () => {
if (!deletingApp) return;
setDeleting(true);
try {
await deleteTeamApplication(deletingApp.id!);
toast({
title: '删除成功',
description: '应用配置已删除',
});
setDeleteDialogOpen(false);
setDeletingApp(null);
loadTeamApplications();
onSuccess?.();
} catch (error: any) {
toast({
title: '删除失败',
description: error.message || '删除应用配置失败',
variant: 'destructive',
});
} finally {
setDeleting(false);
}
await deleteTeamApplication(deletingApp.id!);
};
const getEnvironmentName = (environmentId: number) => {
@ -346,29 +316,25 @@ export const TeamApplicationManageDialog: React.FC<
)}
{/* 删除确认对话框 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"
{deletingApp?.applicationName || `应用 ${deletingApp?.applicationId}`}"
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleting}></AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={deleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{deleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
title="确认删除"
description={`确定要删除应用配置 "${deletingApp?.applicationName || `应用 ${deletingApp?.applicationId}`}" 吗?此操作无法撤销。`}
item={deletingApp}
onConfirm={handleConfirmDelete}
onSuccess={() => {
toast({
title: '删除成功',
description: '应用配置已删除',
});
setDeletingApp(null);
loadTeamApplications();
onSuccess?.();
}}
variant="destructive"
confirmText="删除"
/>
</>
);
};

View File

@ -15,16 +15,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/components/ui/use-toast';
@ -85,7 +76,6 @@ export const TeamEnvironmentManageDialog: React.FC<
const [deletingConfig, setDeletingConfig] = useState<
TeamEnvironmentConfig | null
>(null);
const [deleting, setDeleting] = useState(false);
// 加载团队的环境配置列表
useEffect(() => {
@ -139,28 +129,7 @@ export const TeamEnvironmentManageDialog: React.FC<
const handleConfirmDelete = async () => {
if (!deletingConfig) return;
setDeleting(true);
try {
await deleteTeamEnvironmentConfig(deletingConfig.id);
toast({
title: '删除成功',
description: '环境配置已删除',
});
setDeleteDialogOpen(false);
setDeletingConfig(null);
loadEnvironmentConfigs();
onSuccess?.();
} catch (error: any) {
toast({
title: '删除失败',
description: error.message || '删除环境配置失败',
variant: 'destructive',
});
} finally {
setDeleting(false);
}
await deleteTeamEnvironmentConfig(deletingConfig.id);
};
const handleConfigSuccess = () => {
@ -365,28 +334,25 @@ export const TeamEnvironmentManageDialog: React.FC<
)}
{/* 删除确认对话框 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"{deletingConfig?.environmentName}"
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleting}></AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={deleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{deleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
title="确认删除"
description={`确定要删除环境"${deletingConfig?.environmentName}"的配置吗?此操作将同时删除该环境下的所有应用配置,且无法撤销。`}
item={deletingConfig}
onConfirm={handleConfirmDelete}
onSuccess={() => {
toast({
title: '删除成功',
description: '环境配置已删除',
});
setDeletingConfig(null);
loadEnvironmentConfigs();
onSuccess?.();
}}
variant="destructive"
confirmText="删除"
/>
</>
);
};

View File

@ -43,7 +43,7 @@ import {
Calendar,
User,
GitCommit,
Download,
RotateCw,
} from 'lucide-react';
import type {
RepositoryGroupResponse,
@ -111,6 +111,13 @@ const GitManager: React.FC = () => {
branches: false,
});
// 数量统计
const [projectCount, setProjectCount] = useState(0);
const [branchCount, setBranchCount] = useState(0);
// 刷新触发器
const [projectRefreshKey, setProjectRefreshKey] = useState(0);
const [branchRefreshKey, setBranchRefreshKey] = useState(0);
// 搜索过滤 - 仅保留仓库组搜索
const [groupSearch, setGroupSearch] = useState('');
@ -193,9 +200,6 @@ const GitManager: React.FC = () => {
}
}, [selectedInstanceId, buildTree, toast]);
// 加载项目列表支持不传repoGroupId加载所有项目- 现在不再需要,由 InfiniteScrollList 处理
// 加载分支列表支持不传repoProjectId加载该组所有分支- 现在不再需要,由 InfiniteScrollList 处理
// 同步所有数据(异步任务)
const handleSyncAll = useCallback(async () => {
if (!selectedInstanceId) return;
@ -462,8 +466,6 @@ const GitManager: React.FC = () => {
);
};
// 项目和分支列表现在由 InfiniteScrollList 组件内部管理,不再需要客户端过滤
// 加载 Git 实例列表(只在组件挂载时执行一次)
useEffect(() => {
const loadInstancesEffect = async () => {
@ -513,6 +515,17 @@ const GitManager: React.FC = () => {
const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, filterGroupTree]);
// 缓存 resetTrigger 避免不必要的重置
const projectResetTrigger = React.useMemo(
() => [selectedInstanceId, selectedGroup?.id, projectRefreshKey],
[selectedInstanceId, selectedGroup?.id, projectRefreshKey]
);
const branchResetTrigger = React.useMemo(
() => [selectedInstanceId, selectedProject?.id, branchRefreshKey],
[selectedInstanceId, selectedProject?.id, branchRefreshKey]
);
return (
<TooltipProvider>
@ -576,6 +589,7 @@ const GitManager: React.FC = () => {
className="h-7 w-7"
onClick={loadGroupTree}
disabled={loading.groups || !selectedInstanceId}
title="刷新列表"
>
<RefreshCw
className={`h-4 w-4 ${loading.groups ? 'animate-spin' : ''}`}
@ -587,9 +601,10 @@ const GitManager: React.FC = () => {
className="h-7 w-7"
onClick={handleSyncGroups}
disabled={syncing.groups || !selectedInstanceId}
title="从Git同步"
>
<Download
className={`h-4 w-4 ${syncing.groups ? 'animate-bounce' : ''}`}
<RotateCw
className={`h-4 w-4 ${syncing.groups ? 'animate-spin' : ''}`}
/>
</Button>
</div>
@ -632,22 +647,33 @@ const GitManager: React.FC = () => {
<Card className="col-span-5 flex flex-col min-h-0">
<CardHeader className="border-b flex-shrink-0">
<div className="flex items-center justify-between">
<CardTitle className="text-base">
{selectedGroup && ` - ${selectedGroup.name}`}
<CardTitle className="text-base flex items-center gap-2">
({projectCount}){selectedGroup && ` - ${selectedGroup.name}`}
</CardTitle>
<div className="flex items-center gap-1">
<Button
variant="ghost"
<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
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={handleSyncProjects}
disabled={syncing.projects || !selectedInstanceId}
title="从Git同步"
>
<Download
className={`h-4 w-4 ${syncing.projects ? 'animate-bounce' : ''}`}
<RotateCw
className={`h-4 w-4 ${syncing.projects ? 'animate-spin' : ''}`}
/>
</Button>
</div>
</Button>
</div>
</div>
</CardHeader>
<div
@ -663,6 +689,7 @@ const GitManager: React.FC = () => {
size,
})
}
onDataChange={(data, total) => setProjectCount(data.length)}
renderItem={(project) => (
<div
key={project.id}
@ -734,7 +761,7 @@ const GitManager: React.FC = () => {
)}
scrollableTarget="projectScrollableDiv"
pageSize={20}
resetTrigger={selectedGroup?.id}
resetTrigger={projectResetTrigger}
emptyText={selectedGroup ? '当前仓库组暂无项目' : '暂无项目'}
className="p-2 space-y-2"
/>
@ -745,22 +772,33 @@ const GitManager: React.FC = () => {
<Card className="col-span-4 flex flex-col min-h-0">
<CardHeader className="border-b flex-shrink-0">
<div className="flex items-center justify-between">
<CardTitle className="text-base">
{selectedProject && ` - ${selectedProject.name}`}
<CardTitle className="text-base flex items-center gap-2">
({branchCount}){selectedProject && ` - ${selectedProject.name}`}
</CardTitle>
<div className="flex items-center gap-1">
<Button
variant="ghost"
<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
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={handleSyncBranches}
disabled={syncing.branches || !selectedInstanceId}
title="从Git同步"
>
<Download
className={`h-4 w-4 ${syncing.branches ? 'animate-bounce' : ''}`}
<RotateCw
className={`h-4 w-4 ${syncing.branches ? 'animate-spin' : ''}`}
/>
</Button>
</div>
</Button>
</div>
</div>
</CardHeader>
<div
@ -776,6 +814,7 @@ const GitManager: React.FC = () => {
size,
})
}
onDataChange={(data, total) => setBranchCount(data.length)}
renderItem={(branch) => (
<div key={branch.id} className="p-3 hover:bg-accent transition-colors border-b">
<div className="flex items-start gap-2">
@ -845,7 +884,7 @@ const GitManager: React.FC = () => {
)}
scrollableTarget="branchScrollableDiv"
pageSize={20}
resetTrigger={selectedProject?.id}
resetTrigger={branchResetTrigger}
emptyText={selectedProject ? '当前项目暂无分支' : '暂无分支'}
className=""
/>
@ -859,4 +898,3 @@ const GitManager: React.FC = () => {
};
export default GitManager;

View File

@ -54,7 +54,7 @@ export const getRepositoryProjects = (params: {
}) =>
request.get<Page<RepositoryProjectResponse>>(`${PROJECT_URL}/page`, {
params: {
pageNum: params.pageNum ?? 0,
pageNum: params.pageNum ?? 1,
size: params.size ?? 20,
externalSystemId: params.externalSystemId,
repoGroupId: params.repoGroupId,
@ -84,7 +84,7 @@ export const getRepositoryBranches = (params: {
}) =>
request.get<Page<RepositoryBranchResponse>>(`${BRANCH_URL}/page`, {
params: {
pageNum: params.pageNum ?? 0,
pageNum: params.pageNum ?? 1,
size: params.size ?? 20,
sortField: 'lastUpdateTime',
sortOrder: 'desc',

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ export const getJenkinsInstances = () =>
// 获取视图列表
export const getJenkinsViews = (externalSystemId: number) =>
request.get<JenkinsViewDTO[]>(`/api/v1/jenkins-view`, {
request.get<JenkinsViewDTO[]>(`/api/v1/jenkins-view/list`, {
params: { externalSystemId }
});
@ -28,6 +28,7 @@ export const syncJenkinsViews = (externalSystemId: number) =>
// ==================== Jenkins 任务 ====================
// 获取任务列表(分页)
// 注意pageNum 从 1 开始,后端会自动转换为从 0 开始给 Spring Data JPA
export const getJenkinsJobs = (params: {
externalSystemId: number;
viewId?: number;
@ -36,7 +37,7 @@ export const getJenkinsJobs = (params: {
}) =>
request.get<Page<JenkinsJobDTO>>(`/api/v1/jenkins-job/page`, {
params: {
pageNum: params.pageNum ?? 0,
pageNum: params.pageNum ?? 1,
size: params.size ?? 20,
externalSystemId: params.externalSystemId,
viewId: params.viewId
@ -50,6 +51,7 @@ export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: num
// ==================== Jenkins 构建 ====================
// 获取构建列表(分页,按开始时间倒序排序)
// 注意pageNum 从 1 开始,后端会自动转换为从 0 开始给 Spring Data JPA
export const getJenkinsBuilds = (params: {
externalSystemId: number;
jobId?: number;
@ -58,7 +60,7 @@ export const getJenkinsBuilds = (params: {
}) =>
request.get<Page<JenkinsBuildDTO>>(`/api/v1/jenkins-build/page`, {
params: {
pageNum: params.pageNum ?? 0,
pageNum: params.pageNum ?? 1,
size: params.size ?? 20,
sortField: 'starttime',
sortOrder: 'desc',