增加团队管理页面
This commit is contained in:
parent
7ab6d3354e
commit
0a5c60b888
@ -74,7 +74,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
|||||||
appCode: "",
|
appCode: "",
|
||||||
appName: "",
|
appName: "",
|
||||||
appDesc: "",
|
appDesc: "",
|
||||||
categoryId: undefined,
|
applicationCategoryId: undefined,
|
||||||
language: DevelopmentLanguageTypeEnum.JAVA,
|
language: DevelopmentLanguageTypeEnum.JAVA,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
sort: 0,
|
sort: 0,
|
||||||
@ -131,7 +131,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
|||||||
appCode: initialValues.appCode,
|
appCode: initialValues.appCode,
|
||||||
appName: initialValues.appName,
|
appName: initialValues.appName,
|
||||||
appDesc: initialValues.appDesc || "",
|
appDesc: initialValues.appDesc || "",
|
||||||
categoryId: initialValues.categoryId,
|
applicationCategoryId: initialValues.applicationCategoryId,
|
||||||
language: initialValues.language,
|
language: initialValues.language,
|
||||||
enabled: initialValues.enabled,
|
enabled: initialValues.enabled,
|
||||||
sort: initialValues.sort,
|
sort: initialValues.sort,
|
||||||
@ -221,7 +221,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
|||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="categoryId"
|
name="applicationCategoryId"
|
||||||
render={({field}) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>应用分类</FormLabel>
|
<FormLabel>应用分类</FormLabel>
|
||||||
|
|||||||
@ -104,7 +104,7 @@ const ApplicationList: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const queryParams: ApplicationQuery = {
|
const queryParams: ApplicationQuery = {
|
||||||
...params,
|
...params,
|
||||||
categoryId: selectedCategoryId,
|
applicationCategoryId: selectedCategoryId,
|
||||||
pageNum: pagination.pageNum,
|
pageNum: pagination.pageNum,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export const applicationFormSchema = z.object({
|
|||||||
appCode: z.string().min(1, '应用编码不能为空'),
|
appCode: z.string().min(1, '应用编码不能为空'),
|
||||||
appName: z.string().min(1, '应用名称不能为空'),
|
appName: z.string().min(1, '应用名称不能为空'),
|
||||||
appDesc: z.string().max(200, "应用描述不能超过200个字符").optional(),
|
appDesc: z.string().max(200, "应用描述不能超过200个字符").optional(),
|
||||||
categoryId: z.number({
|
applicationCategoryId: z.number({
|
||||||
required_error: '请选择应用分类',
|
required_error: '请选择应用分类',
|
||||||
invalid_type_error: '应用分类必须是数字',
|
invalid_type_error: '应用分类必须是数字',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export interface Application extends BaseResponse {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
sort: number;
|
sort: number;
|
||||||
teamCount?: number;
|
teamCount?: number;
|
||||||
categoryId: number;
|
applicationCategoryId: number;
|
||||||
category?: ApplicationCategoryResponse;
|
category?: ApplicationCategoryResponse;
|
||||||
externalSystemId?: number;
|
externalSystemId?: number;
|
||||||
externalSystem?: ExternalSystemResponse;
|
externalSystem?: ExternalSystemResponse;
|
||||||
@ -66,7 +66,7 @@ export type ApplicationFormValues = CreateApplicationRequest;
|
|||||||
export interface ApplicationQuery extends BaseQuery {
|
export interface ApplicationQuery extends BaseQuery {
|
||||||
appCode?: string;
|
appCode?: string;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
categoryId?: number;
|
applicationCategoryId?: number;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
language?: string;
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,427 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { DataTablePagination } from '@/components/ui/pagination';
|
||||||
|
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Search,
|
||||||
|
Loader2,
|
||||||
|
Package,
|
||||||
|
Check,
|
||||||
|
ChevronsUpDown,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
getTeamApplications,
|
||||||
|
createTeamApplication,
|
||||||
|
deleteTeamApplication
|
||||||
|
} from '../../Application/service';
|
||||||
|
import type {
|
||||||
|
TeamApplicationResponse,
|
||||||
|
TeamApplicationQuery
|
||||||
|
} from '../../Application/types';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
import type { Application } from '@/pages/Deploy/Application/List/types';
|
||||||
|
|
||||||
|
interface ApplicationManageDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
teamId: number;
|
||||||
|
teamName: string;
|
||||||
|
applications: Application[];
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队应用管理对话框
|
||||||
|
*/
|
||||||
|
const ApplicationManageDialog: React.FC<ApplicationManageDialogProps> = ({
|
||||||
|
open,
|
||||||
|
teamId,
|
||||||
|
teamName,
|
||||||
|
applications,
|
||||||
|
onOpenChange,
|
||||||
|
onSuccess
|
||||||
|
}) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<Page<TeamApplicationResponse> | null>(null);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [deleteRecord, setDeleteRecord] = useState<TeamApplicationResponse | null>(null);
|
||||||
|
const [selectedAppIds, setSelectedAppIds] = useState<number[]>([]);
|
||||||
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
const [adding, setAdding] = useState(false);
|
||||||
|
|
||||||
|
// 分页状态
|
||||||
|
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
|
||||||
|
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||||
|
|
||||||
|
|
||||||
|
// 加载应用列表
|
||||||
|
const loadApplications = async () => {
|
||||||
|
if (!teamId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const query: TeamApplicationQuery = {
|
||||||
|
teamId: teamId,
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
};
|
||||||
|
const result = await getTeamApplications(query);
|
||||||
|
setData(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载应用失败:', error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "加载失败",
|
||||||
|
description: error instanceof Error ? error.message : '未知错误',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
loadApplications();
|
||||||
|
}
|
||||||
|
}, [open, pageNum, pageSize]);
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
setPageNum(0);
|
||||||
|
loadApplications();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换应用选择
|
||||||
|
const toggleAppSelection = (appId: number) => {
|
||||||
|
setSelectedAppIds(prev =>
|
||||||
|
prev.includes(appId)
|
||||||
|
? prev.filter(id => id !== appId)
|
||||||
|
: [...prev, appId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量添加应用
|
||||||
|
const handleBatchAdd = async () => {
|
||||||
|
if (selectedAppIds.length === 0) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "请选择应用",
|
||||||
|
description: "至少选择一个应用进行添加",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAdding(true);
|
||||||
|
try {
|
||||||
|
// 并行创建所有关联
|
||||||
|
await Promise.all(
|
||||||
|
selectedAppIds.map(appId =>
|
||||||
|
createTeamApplication({
|
||||||
|
teamId,
|
||||||
|
applicationId: appId,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "添加成功",
|
||||||
|
description: `已成功添加 ${selectedAppIds.length} 个应用`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedAppIds([]);
|
||||||
|
setPopoverOpen(false);
|
||||||
|
loadApplications();
|
||||||
|
onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量添加失败:', error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "添加失败",
|
||||||
|
description: error instanceof Error ? error.message : '未知错误',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setAdding(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除应用关联(打开确认对话框)
|
||||||
|
const handleDeleteClick = (record: TeamApplicationResponse) => {
|
||||||
|
setDeleteRecord(record);
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
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 : '未知错误',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 过滤出未关联的应用
|
||||||
|
const availableApplications = applications.filter(app =>
|
||||||
|
!data?.content?.some(item => item.applicationId === app.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[85vh] flex flex-col p-0">
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-4">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Package className="h-5 w-5" />
|
||||||
|
团队应用管理 - {teamName}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden flex flex-col px-6 pb-6">
|
||||||
|
<div className="space-y-4 flex-1 overflow-y-auto">
|
||||||
|
{/* 操作栏 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索应用名称..."
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 多选下拉框 */}
|
||||||
|
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={popoverOpen}
|
||||||
|
className="w-[280px] justify-between"
|
||||||
|
disabled={availableApplications.length === 0}
|
||||||
|
>
|
||||||
|
{selectedAppIds.length > 0 ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
已选择 <Badge variant="secondary" className="ml-1">{selectedAppIds.length}</Badge>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
"选择应用..."
|
||||||
|
)}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[280px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="搜索应用..." />
|
||||||
|
<CommandEmpty>未找到应用</CommandEmpty>
|
||||||
|
<CommandGroup className="max-h-[300px] overflow-auto">
|
||||||
|
{availableApplications.map((app) => (
|
||||||
|
<CommandItem
|
||||||
|
key={app.id}
|
||||||
|
onSelect={() => toggleAppSelection(app.id)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedAppIds.includes(app.id)}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<span className="font-medium">{app.appName}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{app.appCode}</span>
|
||||||
|
</div>
|
||||||
|
{selectedAppIds.includes(app.id) && (
|
||||||
|
<Check className="ml-auto h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{/* 批量添加按钮 */}
|
||||||
|
<Button
|
||||||
|
onClick={handleBatchAdd}
|
||||||
|
disabled={selectedAppIds.length === 0 || adding}
|
||||||
|
>
|
||||||
|
{adding ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
添加中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
添加应用
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表格 */}
|
||||||
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
<div className="overflow-x-auto max-h-[50vh]">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[100px]">应用ID</TableHead>
|
||||||
|
<TableHead className="w-[150px]">应用编码</TableHead>
|
||||||
|
<TableHead className="w-[200px]">应用名称</TableHead>
|
||||||
|
<TableHead className="w-[160px]">创建时间</TableHead>
|
||||||
|
<TableHead className="w-[80px] text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-8">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||||
|
<span className="text-sm text-muted-foreground">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : data?.content && data.content.length > 0 ? (
|
||||||
|
data.content.map((record) => (
|
||||||
|
<TableRow key={record.id}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{record.applicationId}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{record.applicationCode || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{record.applicationName || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{record.createTime || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => handleDeleteClick(record)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-12 text-muted-foreground">
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<Package className="h-12 w-12 opacity-20" />
|
||||||
|
<p className="text-sm">暂无应用</p>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{data && data.totalElements > 0 && (
|
||||||
|
<div className="flex justify-end py-3 px-4 border-t bg-muted/40">
|
||||||
|
<DataTablePagination
|
||||||
|
pageIndex={pageNum}
|
||||||
|
pageSize={pageSize}
|
||||||
|
pageCount={Math.ceil((data.totalElements || 0) / pageSize)}
|
||||||
|
onPageChange={(newPage) => setPageNum(newPage)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApplicationManageDialog;
|
||||||
|
|
||||||
@ -17,13 +17,6 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} 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 {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -33,33 +26,36 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import {
|
import {
|
||||||
Form,
|
Command,
|
||||||
FormControl,
|
CommandEmpty,
|
||||||
FormField,
|
CommandGroup,
|
||||||
FormItem,
|
CommandInput,
|
||||||
FormLabel,
|
CommandItem,
|
||||||
FormMessage,
|
} from "@/components/ui/command";
|
||||||
} from "@/components/ui/form";
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { DataTablePagination } from '@/components/ui/pagination';
|
import { DataTablePagination } from '@/components/ui/pagination';
|
||||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Edit,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
Search,
|
Search,
|
||||||
Loader2,
|
Loader2,
|
||||||
Users,
|
Users,
|
||||||
|
Check,
|
||||||
|
ChevronsUpDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
getTeamMembers,
|
getTeamMembers,
|
||||||
createTeamMember,
|
createTeamMember,
|
||||||
updateTeamMember,
|
|
||||||
deleteTeamMember
|
deleteTeamMember
|
||||||
} from '../../Member/service';
|
} from '../../Member/service';
|
||||||
import { teamMemberFormSchema, type TeamMemberFormValues } from '../../Member/schema';
|
|
||||||
import type {
|
import type {
|
||||||
TeamMemberResponse,
|
TeamMemberResponse,
|
||||||
TeamMemberQuery
|
TeamMemberQuery
|
||||||
@ -91,25 +87,16 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState<Page<TeamMemberResponse> | null>(null);
|
const [data, setData] = useState<Page<TeamMemberResponse> | null>(null);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [editMode, setEditMode] = useState(false);
|
|
||||||
const [editRecord, setEditRecord] = useState<TeamMemberResponse | null>(null);
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [deleteRecord, setDeleteRecord] = useState<TeamMemberResponse | null>(null);
|
const [deleteRecord, setDeleteRecord] = useState<TeamMemberResponse | null>(null);
|
||||||
|
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
||||||
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
const [adding, setAdding] = useState(false);
|
||||||
|
|
||||||
// 分页状态
|
// 分页状态
|
||||||
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
|
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
|
||||||
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
|
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||||
|
|
||||||
const form = useForm<TeamMemberFormValues>({
|
|
||||||
resolver: zodResolver(teamMemberFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
teamId: teamId,
|
|
||||||
userId: undefined,
|
|
||||||
userName: '',
|
|
||||||
roleInTeam: '',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// 加载成员列表
|
// 加载成员列表
|
||||||
const loadMembers = async () => {
|
const loadMembers = async () => {
|
||||||
@ -149,28 +136,60 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
|||||||
loadMembers();
|
loadMembers();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 新建成员
|
// 切换用户选择
|
||||||
const handleCreate = () => {
|
const toggleUserSelection = (userId: number) => {
|
||||||
setEditRecord(null);
|
setSelectedUserIds(prev =>
|
||||||
form.reset({
|
prev.includes(userId)
|
||||||
teamId: teamId,
|
? prev.filter(id => id !== userId)
|
||||||
userId: undefined,
|
: [...prev, userId]
|
||||||
userName: '',
|
);
|
||||||
roleInTeam: '',
|
|
||||||
});
|
|
||||||
setEditMode(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 编辑成员
|
// 批量添加成员
|
||||||
const handleEdit = (record: TeamMemberResponse) => {
|
const handleBatchAdd = async () => {
|
||||||
setEditRecord(record);
|
if (selectedUserIds.length === 0) {
|
||||||
form.reset({
|
toast({
|
||||||
teamId: record.teamId,
|
variant: "destructive",
|
||||||
userId: record.userId,
|
title: "请选择成员",
|
||||||
userName: record.userName || '',
|
description: "至少选择一个用户进行添加",
|
||||||
roleInTeam: record.roleInTeam || '',
|
|
||||||
});
|
});
|
||||||
setEditMode(true);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAdding(true);
|
||||||
|
try {
|
||||||
|
// 并行创建所有成员关联
|
||||||
|
await Promise.all(
|
||||||
|
selectedUserIds.map(userId => {
|
||||||
|
const user = users.find(u => u.id === userId);
|
||||||
|
return createTeamMember({
|
||||||
|
teamId,
|
||||||
|
userId,
|
||||||
|
userName: user?.nickname || user?.username || '',
|
||||||
|
roleInTeam: '', // 批量添加时角色为空
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "添加成功",
|
||||||
|
description: `已成功添加 ${selectedUserIds.length} 个成员`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedUserIds([]);
|
||||||
|
setPopoverOpen(false);
|
||||||
|
loadMembers();
|
||||||
|
onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量添加失败:', error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "添加失败",
|
||||||
|
description: error instanceof Error ? error.message : '未知错误',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setAdding(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除成员(打开确认对话框)
|
// 删除成员(打开确认对话框)
|
||||||
@ -203,41 +222,10 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存表单
|
// 过滤出未加入的用户
|
||||||
const handleSave = async (values: TeamMemberFormValues) => {
|
const availableUsers = users.filter(user =>
|
||||||
try {
|
!data?.content?.some(member => member.userId === user.id)
|
||||||
if (editRecord) {
|
);
|
||||||
await updateTeamMember(editRecord.id, values);
|
|
||||||
toast({
|
|
||||||
title: "更新成功",
|
|
||||||
description: `成员 "${values.userName}" 已更新`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await createTeamMember(values);
|
|
||||||
toast({
|
|
||||||
title: "添加成功",
|
|
||||||
description: `成员 "${values.userName}" 已添加`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setEditMode(false);
|
|
||||||
loadMembers();
|
|
||||||
onSuccess?.();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存失败:', error);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "保存失败",
|
|
||||||
description: error instanceof Error ? error.message : '未知错误',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 取消编辑
|
|
||||||
const handleCancel = () => {
|
|
||||||
setEditMode(false);
|
|
||||||
setEditRecord(null);
|
|
||||||
form.reset();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -250,10 +238,9 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{!editMode ? (
|
|
||||||
<div className="flex-1 overflow-hidden flex flex-col px-6 pb-6">
|
<div className="flex-1 overflow-hidden flex flex-col px-6 pb-6">
|
||||||
<div className="space-y-4 flex-1 overflow-y-auto">
|
<div className="space-y-4 flex-1 overflow-y-auto">
|
||||||
{/* 搜索栏 */}
|
{/* 操作栏 */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
@ -265,9 +252,72 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
|||||||
className="pl-10"
|
className="pl-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleCreate}>
|
|
||||||
|
{/* 多选下拉框 */}
|
||||||
|
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={popoverOpen}
|
||||||
|
className="w-[280px] justify-between"
|
||||||
|
disabled={availableUsers.length === 0}
|
||||||
|
>
|
||||||
|
{selectedUserIds.length > 0 ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
已选择 <Badge variant="secondary" className="ml-1">{selectedUserIds.length}</Badge>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
"选择成员..."
|
||||||
|
)}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[280px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="搜索用户..." />
|
||||||
|
<CommandEmpty>未找到用户</CommandEmpty>
|
||||||
|
<CommandGroup className="max-h-[300px] overflow-auto">
|
||||||
|
{availableUsers.map((user) => (
|
||||||
|
<CommandItem
|
||||||
|
key={user.id}
|
||||||
|
onSelect={() => toggleUserSelection(user.id)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedUserIds.includes(user.id)}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<span className="font-medium">{user.nickname || user.username}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{user.username}</span>
|
||||||
|
</div>
|
||||||
|
{selectedUserIds.includes(user.id) && (
|
||||||
|
<Check className="ml-auto h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{/* 批量添加按钮 */}
|
||||||
|
<Button
|
||||||
|
onClick={handleBatchAdd}
|
||||||
|
disabled={selectedUserIds.length === 0 || adding}
|
||||||
|
>
|
||||||
|
{adding ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
添加中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
添加成员
|
添加成员
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -280,8 +330,8 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
|||||||
<TableHead className="w-[100px]">用户ID</TableHead>
|
<TableHead className="w-[100px]">用户ID</TableHead>
|
||||||
<TableHead className="w-[150px]">用户名</TableHead>
|
<TableHead className="w-[150px]">用户名</TableHead>
|
||||||
<TableHead className="w-[150px]">团队角色</TableHead>
|
<TableHead className="w-[150px]">团队角色</TableHead>
|
||||||
<TableHead className="w-[180px]">加入时间</TableHead>
|
<TableHead className="w-[160px]">加入时间</TableHead>
|
||||||
<TableHead className="w-[100px] text-right">操作</TableHead>
|
<TableHead className="w-[80px] text-right">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -311,14 +361,6 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex items-center justify-end gap-1">
|
<div className="flex items-center justify-end gap-1">
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() => handleEdit(record)}
|
|
||||||
>
|
|
||||||
<Edit className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -359,75 +401,6 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="px-6 pb-6">
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(handleSave)} className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="userId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>选择用户</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const userId = Number(value);
|
|
||||||
const selectedUser = users.find(u => u.id === userId);
|
|
||||||
field.onChange(userId);
|
|
||||||
if (selectedUser) {
|
|
||||||
form.setValue('userName', selectedUser.nickname || selectedUser.username);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={field.value?.toString()}
|
|
||||||
disabled={!!editRecord}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="请选择用户" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{users.map((user) => (
|
|
||||||
<SelectItem key={user.id} value={user.id.toString()}>
|
|
||||||
{user.nickname || user.username} ({user.username})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="roleInTeam"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>团队角色</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入团队角色,如:开发、测试、产品经理等"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">
|
|
||||||
{editRecord ? '更新' : '添加'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user