增加团队管理页面
This commit is contained in:
parent
7ab6d3354e
commit
0a5c60b888
@ -74,7 +74,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
appCode: "",
|
||||
appName: "",
|
||||
appDesc: "",
|
||||
categoryId: undefined,
|
||||
applicationCategoryId: undefined,
|
||||
language: DevelopmentLanguageTypeEnum.JAVA,
|
||||
enabled: true,
|
||||
sort: 0,
|
||||
@ -131,7 +131,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
appCode: initialValues.appCode,
|
||||
appName: initialValues.appName,
|
||||
appDesc: initialValues.appDesc || "",
|
||||
categoryId: initialValues.categoryId,
|
||||
applicationCategoryId: initialValues.applicationCategoryId,
|
||||
language: initialValues.language,
|
||||
enabled: initialValues.enabled,
|
||||
sort: initialValues.sort,
|
||||
@ -221,7 +221,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="categoryId"
|
||||
name="applicationCategoryId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>应用分类</FormLabel>
|
||||
|
||||
@ -104,7 +104,7 @@ const ApplicationList: React.FC = () => {
|
||||
try {
|
||||
const queryParams: ApplicationQuery = {
|
||||
...params,
|
||||
categoryId: selectedCategoryId,
|
||||
applicationCategoryId: selectedCategoryId,
|
||||
pageNum: pagination.pageNum,
|
||||
pageSize: pagination.pageSize,
|
||||
};
|
||||
|
||||
@ -12,7 +12,7 @@ export const applicationFormSchema = z.object({
|
||||
appCode: z.string().min(1, '应用编码不能为空'),
|
||||
appName: z.string().min(1, '应用名称不能为空'),
|
||||
appDesc: z.string().max(200, "应用描述不能超过200个字符").optional(),
|
||||
categoryId: z.number({
|
||||
applicationCategoryId: z.number({
|
||||
required_error: '请选择应用分类',
|
||||
invalid_type_error: '应用分类必须是数字',
|
||||
}),
|
||||
|
||||
@ -18,7 +18,7 @@ export interface Application extends BaseResponse {
|
||||
enabled: boolean;
|
||||
sort: number;
|
||||
teamCount?: number;
|
||||
categoryId: number;
|
||||
applicationCategoryId: number;
|
||||
category?: ApplicationCategoryResponse;
|
||||
externalSystemId?: number;
|
||||
externalSystem?: ExternalSystemResponse;
|
||||
@ -66,7 +66,7 @@ export type ApplicationFormValues = CreateApplicationRequest;
|
||||
export interface ApplicationQuery extends BaseQuery {
|
||||
appCode?: string;
|
||||
appName?: string;
|
||||
categoryId?: number;
|
||||
applicationCategoryId?: number;
|
||||
enabled?: boolean;
|
||||
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";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@ -33,33 +26,36 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
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 { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Search,
|
||||
Loader2,
|
||||
Users,
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getTeamMembers,
|
||||
createTeamMember,
|
||||
updateTeamMember,
|
||||
deleteTeamMember
|
||||
} from '../../Member/service';
|
||||
import { teamMemberFormSchema, type TeamMemberFormValues } from '../../Member/schema';
|
||||
import type {
|
||||
TeamMemberResponse,
|
||||
TeamMemberQuery
|
||||
@ -91,25 +87,16 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<Page<TeamMemberResponse> | null>(null);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editRecord, setEditRecord] = useState<TeamMemberResponse | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
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 [pageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
|
||||
const form = useForm<TeamMemberFormValues>({
|
||||
resolver: zodResolver(teamMemberFormSchema),
|
||||
defaultValues: {
|
||||
teamId: teamId,
|
||||
userId: undefined,
|
||||
userName: '',
|
||||
roleInTeam: '',
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 加载成员列表
|
||||
const loadMembers = async () => {
|
||||
@ -149,28 +136,60 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
||||
loadMembers();
|
||||
};
|
||||
|
||||
// 新建成员
|
||||
const handleCreate = () => {
|
||||
setEditRecord(null);
|
||||
form.reset({
|
||||
teamId: teamId,
|
||||
userId: undefined,
|
||||
userName: '',
|
||||
roleInTeam: '',
|
||||
});
|
||||
setEditMode(true);
|
||||
// 切换用户选择
|
||||
const toggleUserSelection = (userId: number) => {
|
||||
setSelectedUserIds(prev =>
|
||||
prev.includes(userId)
|
||||
? prev.filter(id => id !== userId)
|
||||
: [...prev, userId]
|
||||
);
|
||||
};
|
||||
|
||||
// 编辑成员
|
||||
const handleEdit = (record: TeamMemberResponse) => {
|
||||
setEditRecord(record);
|
||||
form.reset({
|
||||
teamId: record.teamId,
|
||||
userId: record.userId,
|
||||
userName: record.userName || '',
|
||||
roleInTeam: record.roleInTeam || '',
|
||||
// 批量添加成员
|
||||
const handleBatchAdd = async () => {
|
||||
if (selectedUserIds.length === 0) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "请选择成员",
|
||||
description: "至少选择一个用户进行添加",
|
||||
});
|
||||
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) => {
|
||||
try {
|
||||
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();
|
||||
};
|
||||
// 过滤出未加入的用户
|
||||
const availableUsers = users.filter(user =>
|
||||
!data?.content?.some(member => member.userId === user.id)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -250,10 +238,9 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{!editMode ? (
|
||||
<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" />
|
||||
@ -265,9 +252,72 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
||||
className="pl-10"
|
||||
/>
|
||||
</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" />
|
||||
添加成员
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -280,8 +330,8 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
||||
<TableHead className="w-[100px]">用户ID</TableHead>
|
||||
<TableHead className="w-[150px]">用户名</TableHead>
|
||||
<TableHead className="w-[150px]">团队角色</TableHead>
|
||||
<TableHead className="w-[180px]">加入时间</TableHead>
|
||||
<TableHead className="w-[100px] text-right">操作</TableHead>
|
||||
<TableHead className="w-[160px]">加入时间</TableHead>
|
||||
<TableHead className="w-[80px] text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@ -311,14 +361,6 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
||||
</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={() => handleEdit(record)}
|
||||
>
|
||||
<Edit className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@ -359,75 +401,6 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
|
||||
</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>
|
||||
</Dialog>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user