重构前端逻辑
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;
|
||||||
|
await deleteTeamApplication(deleteRecord.id);
|
||||||
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 : '未知错误',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 过滤出未关联的应用
|
// 过滤出未关联的应用
|
||||||
@ -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,92 +25,83 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
|||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
if (!record) return;
|
if (!record) return;
|
||||||
|
await deleteTeam(record.id);
|
||||||
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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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={
|
||||||
<AlertCircle className="h-5 w-5 text-destructive" />
|
<div className="flex items-center gap-2">
|
||||||
确认删除
|
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||||
</DialogTitle>
|
确认删除
|
||||||
<DialogDescription>
|
</div>
|
||||||
此操作将永久删除该团队,该操作无法撤销。
|
}
|
||||||
</DialogDescription>
|
description="此操作将永久删除该团队,该操作无法撤销。"
|
||||||
</DialogHeader>
|
item={record}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
<div className="space-y-3 py-4">
|
onSuccess={() => {
|
||||||
<div className="rounded-lg bg-muted p-4 space-y-2">
|
toast({
|
||||||
<div className="flex justify-between items-center">
|
title: '删除成功',
|
||||||
<span className="text-sm text-muted-foreground">团队编码:</span>
|
description: `团队 "${record.teamName}" 已删除`
|
||||||
<code className="text-sm font-mono bg-background px-2 py-1 rounded">
|
});
|
||||||
{record.teamCode}
|
onSuccess();
|
||||||
</code>
|
}}
|
||||||
</div>
|
variant="destructive"
|
||||||
<div className="flex justify-between items-center">
|
confirmText="确认删除"
|
||||||
<span className="text-sm text-muted-foreground">团队名称:</span>
|
maxWidth="md"
|
||||||
<span className="text-sm font-medium">{record.teamName}</span>
|
>
|
||||||
</div>
|
<div className="space-y-3">
|
||||||
{record.ownerName && (
|
<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>
|
||||||
<span className="text-sm">{record.ownerName}</span>
|
<code className="text-sm font-mono bg-background px-2 py-1 rounded">
|
||||||
</div>
|
{record.teamCode}
|
||||||
)}
|
</code>
|
||||||
{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>
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
{((record.memberCount !== undefined && record.memberCount > 0) ||
|
<span className="text-sm text-muted-foreground">团队名称:</span>
|
||||||
(record.applicationCount !== undefined && record.applicationCount > 0)) && (
|
<span className="text-sm font-medium">{record.teamName}</span>
|
||||||
<div className="flex items-start gap-2 p-3 rounded-lg bg-destructive/10 text-destructive">
|
</div>
|
||||||
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
{record.ownerName && (
|
||||||
<p className="text-sm">
|
<div className="flex justify-between items-center">
|
||||||
警告:该团队还有{record.memberCount || 0}名成员和{record.applicationCount || 0}个关联应用,删除后将影响这些关联数据。
|
<span className="text-sm text-muted-foreground">负责人:</span>
|
||||||
</p>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
{((record.memberCount !== undefined && record.memberCount > 0) ||
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
(record.applicationCount !== undefined && record.applicationCount > 0)) && (
|
||||||
取消
|
<div className="flex items-start gap-2 p-3 rounded-lg bg-destructive/10 text-destructive">
|
||||||
</Button>
|
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||||
<Button variant="destructive" onClick={handleConfirm}>
|
<p className="text-sm">
|
||||||
确认删除
|
警告:该团队还有{record.memberCount || 0}名成员和{record.applicationCount || 0}个关联应用,删除后将影响这些关联数据。
|
||||||
</Button>
|
</p>
|
||||||
</DialogFooter>
|
</div>
|
||||||
</DialogContent>
|
)}
|
||||||
</Dialog>
|
</div>
|
||||||
|
</ConfirmDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
await deleteTeamMember(deleteRecord.id);
|
||||||
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 : '未知错误',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 过滤出未加入的用户(排除负责人和已加入的成员)
|
// 过滤出未加入的用户(排除负责人和已加入的成员)
|
||||||
@ -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;
|
||||||
|
await deleteTeamApplication(deletingApp.id!);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
await deleteTeamEnvironmentConfig(deletingConfig.id);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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,22 +647,33 @@ 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
|
<Button
|
||||||
variant="ghost"
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<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,22 +772,33 @@ 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
|
<Button
|
||||||
variant="ghost"
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<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',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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