重构消息通知弹窗

This commit is contained in:
dengqichen 2025-11-28 16:47:06 +08:00
parent 52d1f0ef27
commit 38ebe57a13

View File

@ -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 { PageContainer } from '@/components/ui/page-container';
import { getTeams } from './service'; import { getTeams } from './service';
import type { TeamResponse, TeamQuery } from './types'; import type { TeamResponse, TeamQuery } from './types';
@ -10,44 +10,17 @@ import type { UserResponse } from '@/pages/System/User/List/types';
import { getEnvironmentList } from './service'; import { getEnvironmentList } from './service';
import type { Environment } from './types'; import type { Environment } from './types';
import { TeamEnvironmentManageDialog } from './components/TeamEnvironmentManageDialog'; import { TeamEnvironmentManageDialog } from './components/TeamEnvironmentManageDialog';
import { import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
Table,
TableHeader,
TableBody,
TableHead,
TableRow,
TableCell,
} from '@/components/ui/table';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge';
import { import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } 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 { import {
Plus, Plus,
Edit, Edit,
@ -60,23 +33,8 @@ import {
Settings, Settings,
} from 'lucide-react'; } 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 TeamList: React.FC = () => {
const [list, setList] = useState<TeamResponse[]>([]); const tableRef = useRef<PaginatedTableRef<TeamResponse>>(null);
const [pagination, setPagination] = useState({
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [currentTeam, setCurrentTeam] = useState<TeamResponse | undefined>(); const [currentTeam, setCurrentTeam] = useState<TeamResponse | undefined>();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@ -84,48 +42,7 @@ const TeamList: React.FC = () => {
const [envManageDialogOpen, setEnvManageDialogOpen] = useState(false); const [envManageDialogOpen, setEnvManageDialogOpen] = useState(false);
const [users, setUsers] = useState<UserResponse[]>([]); const [users, setUsers] = useState<UserResponse[]>([]);
const [environments, setEnvironments] = useState<Environment[]>([]); const [environments, setEnvironments] = useState<Environment[]>([]);
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
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);
}
};
// 加载基础数据(仅一次) // 加载基础数据(仅一次)
useEffect(() => { useEffect(() => {
@ -155,13 +72,21 @@ const TeamList: React.FC = () => {
} }
}; };
// 包装 fetchFn同时更新统计数据
useEffect(() => { const fetchData = async (query: TeamQuery) => {
loadData(); const result = await getTeams(query);
}, [pagination.pageNum, pagination.pageSize]); // 更新统计
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 = () => { const handleSuccess = () => {
loadData(); tableRef.current?.refresh();
}; };
const handleAdd = () => { 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', key: 'ownerName',
header: 'ID', title: '负责人',
size: 80, width: '140px',
render: (_, record) => record.ownerName || '-',
}, },
{ {
accessorKey: 'teamCode', key: 'memberCount',
header: '团队编码', title: '成员数量',
size: 180, width: '100px',
}, render: (_, record) => (
{
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 }) => (
<div className="text-center"> <div className="text-center">
<span className="font-medium">{row.original.memberCount || 0}</span> <span className="font-medium">{record.memberCount || 0}</span>
</div> </div>
), ),
}, },
{ {
id: 'environmentCount', key: 'environmentCount',
header: '环境数量', title: '环境数量',
size: 100, width: '100px',
cell: ({ row }) => ( render: (_, record) => (
<div className="text-center"> <div className="text-center">
<Badge variant="outline" className="font-medium"> <Badge variant="outline" className="font-medium">
{row.original.environmentCount || 0} {record.environmentCount || 0}
</Badge> </Badge>
</div> </div>
), ),
}, },
{ {
id: 'applicationCount', key: 'applicationCount',
header: '应用数量', title: '应用数量',
size: 100, width: '100px',
cell: ({ row }) => ( render: (_, record) => (
<div className="text-center"> <div className="text-center">
<Badge variant="outline" className="font-medium"> <Badge variant="outline" className="font-medium">
{row.original.applicationCount || 0} {record.applicationCount || 0}
</Badge> </Badge>
</div> </div>
), ),
}, },
{ {
accessorKey: 'enabled', key: 'enabled',
header: '状态', title: '状态',
size: 100, width: '100px',
cell: ({ row }) => ( render: (_, record) => (
<Badge variant={row.original.enabled ? 'default' : 'secondary'} className="inline-flex"> <Badge variant={record.enabled ? 'default' : 'secondary'} className="inline-flex">
{row.original.enabled ? '启用' : '禁用'} {record.enabled ? '启用' : '禁用'}
</Badge> </Badge>
), ),
}, },
{ key: 'sort', title: '排序', dataIndex: 'sort', width: '100px' },
{ {
accessorKey: 'sort', key: 'actions',
header: '排序', title: '操作',
size: 100, width: '120px',
}, sticky: true,
{ render: (_, record) => (
id: 'actions',
header: '操作',
size: 180,
cell: ({ row }) => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -290,20 +209,20 @@ const TeamList: React.FC = () => {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleManageEnvironments(row.original)}> <DropdownMenuItem onClick={() => handleManageEnvironments(record)}>
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => handleManageMembers(row.original)}> <DropdownMenuItem onClick={() => handleManageMembers(record)}>
<Users className="mr-2 h-4 w-4" /> <Users className="mr-2 h-4 w-4" />
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(row.original)}> <DropdownMenuItem onClick={() => handleEdit(record)}>
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleDelete(row.original)} onClick={() => handleDelete(record)}
className="text-destructive focus:text-destructive" className="text-destructive focus:text-destructive"
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
@ -314,7 +233,15 @@ const TeamList: React.FC = () => {
</div> </div>
), ),
}, },
]; ], []);
// 工具栏
const toolbar = (
<Button onClick={handleAdd}>
<Plus className="h-4 w-4 mr-2" />
</Button>
);
return ( return (
<PageContainer> <PageContainer>
@ -355,115 +282,24 @@ const TeamList: React.FC = () => {
{/* 团队管理 */} {/* 团队管理 */}
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div>
<div> <CardTitle></CardTitle>
<CardTitle></CardTitle> <CardDescription className="mt-2">
<CardDescription className="mt-2">
</CardDescription>
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button onClick={handleAdd}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</div> </div>
</CardHeader> </CardHeader>
<Separator /> <Separator />
<CardContent className="pt-6"> <CardContent className="pt-6">
{/* 搜索过滤 */} <PaginatedTable<TeamResponse, TeamQuery>
<div className="flex items-center gap-4 mb-6"> ref={tableRef}
<Input fetchFn={fetchData}
placeholder="团队编码" columns={columns}
value={form.watch('teamCode')} searchFields={searchFields}
onChange={(e) => form.setValue('teamCode', e.target.value)} toolbar={toolbar}
className="max-w-[200px]" rowKey="id"
/> minWidth="1440px"
<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>
</CardContent> </CardContent>
</Card> </Card>
@ -507,4 +343,3 @@ const TeamList: React.FC = () => {
}; };
export default TeamList; export default TeamList;