重构消息通知弹窗
This commit is contained in:
parent
52d1f0ef27
commit
38ebe57a13
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { PageContainer } from '@/components/ui/page-container';
|
||||
import { getTeams } from './service';
|
||||
import type { TeamResponse, TeamQuery } from './types';
|
||||
@ -10,44 +10,17 @@ import type { UserResponse } from '@/pages/System/User/List/types';
|
||||
import { getEnvironmentList } from './service';
|
||||
import type { Environment } from './types';
|
||||
import { TeamEnvironmentManageDialog } from './components/TeamEnvironmentManageDialog';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { searchFormSchema, type SearchFormValues } from './schema';
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
|
||||
import type { Page } from '@/types/base';
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
@ -60,23 +33,8 @@ import {
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
|
||||
|
||||
type Column = {
|
||||
accessorKey?: keyof TeamResponse;
|
||||
id?: string;
|
||||
header: string;
|
||||
size?: number;
|
||||
cell?: (props: { row: { original: TeamResponse } }) => React.ReactNode;
|
||||
};
|
||||
|
||||
const TeamList: React.FC = () => {
|
||||
const [list, setList] = useState<TeamResponse[]>([]);
|
||||
const [pagination, setPagination] = useState({
|
||||
pageNum: DEFAULT_PAGE_NUM,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
totalElements: 0,
|
||||
});
|
||||
|
||||
const tableRef = useRef<PaginatedTableRef<TeamResponse>>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentTeam, setCurrentTeam] = useState<TeamResponse | undefined>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@ -84,48 +42,7 @@ const TeamList: React.FC = () => {
|
||||
const [envManageDialogOpen, setEnvManageDialogOpen] = useState(false);
|
||||
const [users, setUsers] = useState<UserResponse[]>([]);
|
||||
const [environments, setEnvironments] = useState<Environment[]>([]);
|
||||
|
||||
const form = useForm<SearchFormValues>({
|
||||
resolver: zodResolver(searchFormSchema),
|
||||
defaultValues: {
|
||||
teamCode: '',
|
||||
teamName: '',
|
||||
enabled: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// 统计数据
|
||||
const stats = useMemo(() => {
|
||||
const total = pagination.totalElements;
|
||||
const enabled = list.filter((item) => item.enabled).length;
|
||||
const disabled = list.filter((item) => !item.enabled).length;
|
||||
return { total, enabled, disabled };
|
||||
}, [list, pagination.totalElements]);
|
||||
|
||||
// 加载数据
|
||||
const loadData = async (searchValues?: SearchFormValues) => {
|
||||
try {
|
||||
const query: TeamQuery = {
|
||||
pageNum: pagination.pageNum,
|
||||
pageSize: pagination.pageSize,
|
||||
teamCode: searchValues?.teamCode || form.getValues('teamCode') || undefined,
|
||||
teamName: searchValues?.teamName || form.getValues('teamName') || undefined,
|
||||
enabled: searchValues?.enabled !== undefined ? searchValues.enabled : form.getValues('enabled'),
|
||||
};
|
||||
|
||||
const response: Page<TeamResponse> | undefined = await getTeams(query);
|
||||
|
||||
if (response) {
|
||||
setList(response.content || []);
|
||||
setPagination({
|
||||
...pagination,
|
||||
totalElements: response.totalElements || 0,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
}
|
||||
};
|
||||
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
|
||||
|
||||
// 加载基础数据(仅一次)
|
||||
useEffect(() => {
|
||||
@ -155,13 +72,21 @@ const TeamList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [pagination.pageNum, pagination.pageSize]);
|
||||
// 包装 fetchFn,同时更新统计数据
|
||||
const fetchData = async (query: TeamQuery) => {
|
||||
const result = await getTeams(query);
|
||||
// 更新统计
|
||||
const all = result.content || [];
|
||||
setStats({
|
||||
total: result.totalElements || 0,
|
||||
enabled: all.filter((item) => item.enabled).length,
|
||||
disabled: all.filter((item) => !item.enabled).length,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
loadData();
|
||||
tableRef.current?.refresh();
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
@ -196,91 +121,85 @@ const TeamList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPagination({ ...pagination, pageNum: newPage + 1 });
|
||||
};
|
||||
// 搜索字段定义
|
||||
const searchFields: SearchFieldDef[] = useMemo(() => [
|
||||
{ key: 'teamCode', type: 'input', placeholder: '团队编码', width: 'w-[180px]' },
|
||||
{ key: 'teamName', type: 'input', placeholder: '团队名称', width: 'w-[180px]' },
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'select',
|
||||
placeholder: '状态',
|
||||
width: 'w-[120px]',
|
||||
options: [
|
||||
{ label: '启用', value: 'true' },
|
||||
{ label: '禁用', value: 'false' },
|
||||
],
|
||||
},
|
||||
], []);
|
||||
|
||||
const columns: Column[] = [
|
||||
// 列定义
|
||||
const columns: ColumnDef<TeamResponse>[] = useMemo(() => [
|
||||
{ key: 'id', title: 'ID', dataIndex: 'id', width: '80px' },
|
||||
{ key: 'teamCode', title: '团队编码', dataIndex: 'teamCode', width: '180px' },
|
||||
{ key: 'teamName', title: '团队名称', dataIndex: 'teamName', width: '180px' },
|
||||
{ key: 'description', title: '团队描述', dataIndex: 'description', width: '260px' },
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
size: 80,
|
||||
key: 'ownerName',
|
||||
title: '负责人',
|
||||
width: '140px',
|
||||
render: (_, record) => record.ownerName || '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'teamCode',
|
||||
header: '团队编码',
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
accessorKey: 'teamName',
|
||||
header: '团队名称',
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: '团队描述',
|
||||
size: 260,
|
||||
},
|
||||
{
|
||||
accessorKey: 'ownerName',
|
||||
header: '负责人',
|
||||
size: 140,
|
||||
cell: ({ row }) => row.original.ownerName || '-',
|
||||
},
|
||||
{
|
||||
id: 'memberCount',
|
||||
header: '成员数量',
|
||||
size: 100,
|
||||
cell: ({ row }) => (
|
||||
key: 'memberCount',
|
||||
title: '成员数量',
|
||||
width: '100px',
|
||||
render: (_, record) => (
|
||||
<div className="text-center">
|
||||
<span className="font-medium">{row.original.memberCount || 0}</span>
|
||||
<span className="font-medium">{record.memberCount || 0}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'environmentCount',
|
||||
header: '环境数量',
|
||||
size: 100,
|
||||
cell: ({ row }) => (
|
||||
key: 'environmentCount',
|
||||
title: '环境数量',
|
||||
width: '100px',
|
||||
render: (_, record) => (
|
||||
<div className="text-center">
|
||||
<Badge variant="outline" className="font-medium">
|
||||
{row.original.environmentCount || 0}
|
||||
{record.environmentCount || 0}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'applicationCount',
|
||||
header: '应用数量',
|
||||
size: 100,
|
||||
cell: ({ row }) => (
|
||||
key: 'applicationCount',
|
||||
title: '应用数量',
|
||||
width: '100px',
|
||||
render: (_, record) => (
|
||||
<div className="text-center">
|
||||
<Badge variant="outline" className="font-medium">
|
||||
{row.original.applicationCount || 0}
|
||||
{record.applicationCount || 0}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'enabled',
|
||||
header: '状态',
|
||||
size: 100,
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.original.enabled ? 'default' : 'secondary'} className="inline-flex">
|
||||
{row.original.enabled ? '启用' : '禁用'}
|
||||
key: 'enabled',
|
||||
title: '状态',
|
||||
width: '100px',
|
||||
render: (_, record) => (
|
||||
<Badge variant={record.enabled ? 'default' : 'secondary'} className="inline-flex">
|
||||
{record.enabled ? '启用' : '禁用'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{ key: 'sort', title: '排序', dataIndex: 'sort', width: '100px' },
|
||||
{
|
||||
accessorKey: 'sort',
|
||||
header: '排序',
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '操作',
|
||||
size: 180,
|
||||
cell: ({ row }) => (
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
width: '120px',
|
||||
sticky: true,
|
||||
render: (_, record) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@ -290,20 +209,20 @@ const TeamList: React.FC = () => {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleManageEnvironments(row.original)}>
|
||||
<DropdownMenuItem onClick={() => handleManageEnvironments(record)}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
环境管理
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleManageMembers(row.original)}>
|
||||
<DropdownMenuItem onClick={() => handleManageMembers(record)}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
管理成员
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
|
||||
<DropdownMenuItem onClick={() => handleEdit(record)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
编辑团队
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(row.original)}
|
||||
onClick={() => handleDelete(record)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
@ -314,7 +233,15 @@ const TeamList: React.FC = () => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
], []);
|
||||
|
||||
// 工具栏
|
||||
const toolbar = (
|
||||
<Button onClick={handleAdd}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建团队
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
@ -355,115 +282,24 @@ const TeamList: React.FC = () => {
|
||||
{/* 团队管理 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>团队管理</CardTitle>
|
||||
<CardDescription className="mt-2">
|
||||
创建和管理团队,分配团队成员和应用
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={handleAdd}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建团队
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>团队管理</CardTitle>
|
||||
<CardDescription className="mt-2">
|
||||
创建和管理团队,分配团队成员和应用
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Separator />
|
||||
<CardContent className="pt-6">
|
||||
{/* 搜索过滤 */}
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Input
|
||||
placeholder="团队编码"
|
||||
value={form.watch('teamCode')}
|
||||
onChange={(e) => form.setValue('teamCode', e.target.value)}
|
||||
className="max-w-[200px]"
|
||||
/>
|
||||
<Input
|
||||
placeholder="团队名称"
|
||||
value={form.watch('teamName')}
|
||||
onChange={(e) => form.setValue('teamName', e.target.value)}
|
||||
className="max-w-[200px]"
|
||||
/>
|
||||
<Select
|
||||
value={form.watch('enabled')?.toString()}
|
||||
onValueChange={(value) => form.setValue('enabled', value === 'true')}
|
||||
>
|
||||
<SelectTrigger className="max-w-[200px]">
|
||||
<SelectValue placeholder="状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">启用</SelectItem>
|
||||
<SelectItem value="false">禁用</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" onClick={() => form.reset()}>
|
||||
重置
|
||||
</Button>
|
||||
<Button onClick={() => loadData(form.getValues())}>搜索</Button>
|
||||
</div>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<div className="rounded-md border">
|
||||
<Table minWidth="1440px">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableHead
|
||||
key={column.accessorKey || column.id}
|
||||
width={column.size ? `${column.size}px` : undefined}
|
||||
sticky={column.id === 'actions'}
|
||||
>
|
||||
{column.header}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
list.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
{columns.map((column) => {
|
||||
let cellContent;
|
||||
if (column.cell) {
|
||||
cellContent = column.cell({ row: { original: item } });
|
||||
} else if (column.accessorKey) {
|
||||
const value = item[column.accessorKey];
|
||||
cellContent = value != null ? String(value) : '';
|
||||
} else {
|
||||
cellContent = '';
|
||||
}
|
||||
return (
|
||||
<TableCell
|
||||
key={column.accessorKey || column.id}
|
||||
width={column.size ? `${column.size}px` : undefined}
|
||||
sticky={column.id === 'actions'}
|
||||
>
|
||||
{cellContent}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex justify-end border-t border-border bg-muted/40">
|
||||
<DataTablePagination
|
||||
pageIndex={pagination.pageNum}
|
||||
pageSize={pagination.pageSize}
|
||||
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginatedTable<TeamResponse, TeamQuery>
|
||||
ref={tableRef}
|
||||
fetchFn={fetchData}
|
||||
columns={columns}
|
||||
searchFields={searchFields}
|
||||
toolbar={toolbar}
|
||||
rowKey="id"
|
||||
minWidth="1440px"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -507,4 +343,3 @@ const TeamList: React.FC = () => {
|
||||
};
|
||||
|
||||
export default TeamList;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user