重构消息通知弹窗

This commit is contained in:
dengqichen 2025-11-28 14:40:50 +08:00
parent 0795570470
commit 6c9df6dd58

View File

@ -1,8 +1,7 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { PageContainer } from '@/components/ui/page-container';
import {
CodeOutlined,
GithubOutlined,
JavaOutlined,
NodeIndexOutlined,
PythonOutlined,
@ -15,75 +14,37 @@ import type { ApplicationCategoryResponse } from '../Category/types';
import ApplicationModal from './components/ApplicationModal';
import DeleteDialog from './components/DeleteDialog';
import CategoryManageDialog from './components/CategoryManageDialog';
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 { useToast } from '@/components/ui/use-toast';
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 { Plus, Edit, Trash2, Server, Activity, Database, ExternalLink, Settings } from 'lucide-react';
import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
import { Plus, Edit, Trash2, Server, Activity, Database, Settings } from 'lucide-react';
interface Column {
accessorKey?: keyof Application;
id?: string;
header: string;
size: number;
cell?: (props: { row: { original: Application } }) => React.ReactNode;
}
const DEFAULT_PAGE_SIZE = 10;
// 获取开发语言信息
const getLanguageInfo = (language: DevelopmentLanguageTypeEnum) => {
switch (language) {
case DevelopmentLanguageTypeEnum.JAVA:
return { label: 'Java', icon: <JavaOutlined />, color: '#E76F00' };
case DevelopmentLanguageTypeEnum.NODE_JS:
return { label: 'NodeJS', icon: <NodeIndexOutlined />, color: '#339933' };
case DevelopmentLanguageTypeEnum.PYTHON:
return { label: 'Python', icon: <PythonOutlined />, color: '#3776AB' };
case DevelopmentLanguageTypeEnum.GO:
return { label: 'Go', icon: <CodeOutlined />, color: '#00ADD8' };
default:
return { label: language || '未知', icon: <CodeOutlined />, color: '#666666' };
}
};
const ApplicationList: React.FC = () => {
const tableRef = useRef<PaginatedTableRef<Application>>(null);
const [modalVisible, setModalVisible] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
const [currentApplication, setCurrentApplication] = useState<Application>();
const [list, setList] = useState<Application[]>([]);
const [categories, setCategories] = useState<ApplicationCategoryResponse[]>([]);
const [loading, setLoading] = useState(false);
const [selectedCategoryId, setSelectedCategoryId] = useState<number>();
const [pagination, setPagination] = useState({
pageNum: 0,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
const { toast } = useToast();
const form = useForm<SearchFormValues>({
resolver: zodResolver(searchFormSchema),
defaultValues: {
appCode: '',
appName: '',
language: undefined,
enabled: undefined,
},
});
// 加载分类列表
const loadCategories = async () => {
@ -99,50 +60,19 @@ const ApplicationList: React.FC = () => {
loadCategories();
}, []);
const loadData = async (params?: ApplicationQuery) => {
setLoading(true);
try {
const queryParams: ApplicationQuery = {
...params,
applicationCategoryId: selectedCategoryId,
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
};
const data = await getApplicationPage(queryParams);
setList(data.content || []);
setPagination({
...pagination,
totalElements: data.totalElements,
});
// 计算统计数据
const all = data.content || [];
setStats({
total: all.length,
enabled: all.filter((item) => item.enabled).length,
disabled: all.filter((item) => !item.enabled).length,
});
} catch (error) {
toast({
variant: 'destructive',
title: '获取应用列表失败',
duration: 3000,
});
} finally {
setLoading(false);
}
};
const handlePageChange = (page: number) => {
setPagination({
...pagination,
pageNum: page,
// 包装 fetchFn同时更新统计数据
const fetchData = async (query: ApplicationQuery) => {
const result = await getApplicationPage(query);
// 更新统计
const all = result.content || [];
setStats({
total: all.length,
enabled: all.filter((item) => item.enabled).length,
disabled: all.filter((item) => !item.enabled).length,
});
return result;
};
useEffect(() => {
loadData(form.getValues());
}, [selectedCategoryId, pagination.pageNum, pagination.pageSize]);
const handleAdd = () => {
setCurrentApplication(undefined);
setModalVisible(true);
@ -161,87 +91,71 @@ const ApplicationList: React.FC = () => {
const handleSuccess = () => {
setModalVisible(false);
setCurrentApplication(undefined);
loadData(form.getValues());
tableRef.current?.refresh();
};
// 获取开发语言信息
const getLanguageInfo = (language: DevelopmentLanguageTypeEnum) => {
switch (language) {
case DevelopmentLanguageTypeEnum.JAVA:
return {
label: 'Java',
icon: <JavaOutlined />,
color: '#E76F00',
};
case DevelopmentLanguageTypeEnum.NODE_JS:
return {
label: 'NodeJS',
icon: <NodeIndexOutlined />,
color: '#339933',
};
case DevelopmentLanguageTypeEnum.PYTHON:
return {
label: 'Python',
icon: <PythonOutlined />,
color: '#3776AB',
};
case DevelopmentLanguageTypeEnum.GO:
return {
label: 'Go',
icon: <CodeOutlined />,
color: '#00ADD8',
};
default:
return {
label: language || '未知',
icon: <CodeOutlined />,
color: '#666666',
};
}
};
// 搜索字段定义
const searchFields: SearchFieldDef[] = useMemo(() => [
{ key: 'appCode', type: 'input', placeholder: '应用编码', width: 'w-[180px]' },
{ key: 'appName', type: 'input', placeholder: '应用名称', width: 'w-[180px]' },
{
key: 'applicationCategoryId',
type: 'select',
placeholder: '全部分类',
width: 'w-[180px]',
options: categories.map(c => ({ label: c.name, value: c.id })),
},
{
key: 'language',
type: 'select',
placeholder: '开发语言',
width: 'w-[150px]',
options: [
{ label: 'Java', value: DevelopmentLanguageTypeEnum.JAVA },
{ label: 'NodeJS', value: DevelopmentLanguageTypeEnum.NODE_JS },
{ label: 'Python', value: DevelopmentLanguageTypeEnum.PYTHON },
{ label: 'Go', value: DevelopmentLanguageTypeEnum.GO },
],
},
{
key: 'enabled',
type: 'select',
placeholder: '状态',
width: 'w-[120px]',
options: [
{ label: '启用', value: 'true' },
{ label: '禁用', value: 'false' },
],
},
], [categories]);
const columns: Column[] = [
// 列定义
const columns: ColumnDef<Application>[] = useMemo(() => [
{
id: 'category',
header: '应用分类',
size: 120,
cell: ({ row }) => (
<div className="flex items-center gap-2">
<span>{row.original.applicationCategory?.name || '-'}</span>
</div>
),
key: 'category',
title: '应用分类',
width: '120px',
render: (_, record) => record.applicationCategory?.name || '-',
},
{ key: 'appCode', title: '应用编码', dataIndex: 'appCode', width: '200px' },
{ key: 'appName', title: '应用名称', dataIndex: 'appName', width: '130px' },
{ key: 'appDesc', title: '应用描述', dataIndex: 'appDesc', width: '180px' },
{
accessorKey: 'appCode',
header: '应用编码',
size: 200,
},
{
accessorKey: 'appName',
header: '应用名称',
size: 130,
},
{
accessorKey: 'appDesc',
header: '应用描述',
size: 180,
},
{
id: 'teamCount',
header: '团队使用数量',
size: 120,
cell: ({ row }) => (
key: 'teamCount',
title: '团队使用数量',
width: '120px',
render: (_, record) => (
<div className="text-center">
<span className="font-medium">{row.original.teamCount || 0}</span>
<span className="font-medium">{record.teamCount || 0}</span>
</div>
),
},
{
accessorKey: 'language',
header: '开发语言',
size: 120,
cell: ({ row }) => {
const langInfo = getLanguageInfo(row.original.language);
key: 'language',
title: '开发语言',
width: '120px',
render: (_, record) => {
const langInfo = getLanguageInfo(record.language);
return (
<Badge variant="outline" className="flex items-center gap-1 inline-flex">
{langInfo.icon}
@ -251,46 +165,53 @@ const ApplicationList: React.FC = () => {
},
},
{
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: '80px' },
{
accessorKey: 'sort',
header: '排序',
size: 80,
},
{
id: 'actions',
header: '操作',
size: 180,
cell: ({ row }) => (
key: 'actions',
title: '操作',
width: '180px',
sticky: true,
render: (_, record) => (
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => handleEdit(row.original)}>
<Edit className="h-4 w-4 mr-1" />
<Button variant="ghost" size="sm" onClick={() => handleEdit(record)}>
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setCurrentApplication(row.original);
setCurrentApplication(record);
setDeleteDialogOpen(true);
}}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4 mr-1" />
<Trash2 className="h-4 w-4 mr-1" />
</Button>
</div>
),
},
];
], []);
// 工具栏
const toolbar = (
<>
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
<Settings className="h-4 w-4 mr-2" />
</Button>
<Button onClick={handleAdd}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</>
);
return (
<PageContainer>
@ -331,167 +252,24 @@ const ApplicationList: React.FC = () => {
{/* 应用管理 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle></CardTitle>
<CardDescription className="mt-2">
Git集成和分类管理
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
<Settings className="h-4 w-4 mr-2" />
</Button>
<Button onClick={handleAdd}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
<div>
<CardTitle></CardTitle>
<CardDescription className="mt-2">
Git集成和分类管理
</CardDescription>
</div>
</CardHeader>
<Separator />
<CardContent className="pt-6">
{/* 搜索过滤 */}
<div className="flex items-center gap-4 mb-6">
<Input
placeholder="应用编码"
value={form.watch('appCode')}
onChange={(e) => form.setValue('appCode', e.target.value)}
className="max-w-[200px]"
/>
<Input
placeholder="应用名称"
value={form.watch('appName')}
onChange={(e) => form.setValue('appName', e.target.value)}
className="max-w-[200px]"
/>
<Select
value={selectedCategoryId?.toString()}
onValueChange={(value) => {
if (value === "all") {
setSelectedCategoryId(undefined);
} else {
setSelectedCategoryId(Number(value));
}
setPagination({ ...pagination, pageNum: 0 });
}}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="全部分类" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id.toString()}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={form.watch('language')}
onValueChange={(value) =>
form.setValue('language', value as DevelopmentLanguageTypeEnum)
}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="开发语言" />
</SelectTrigger>
<SelectContent>
<SelectItem value={DevelopmentLanguageTypeEnum.JAVA}>Java</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.NODE_JS}>NodeJS</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.PYTHON}>Python</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.GO}>Go</SelectItem>
</SelectContent>
</Select>
<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="1400px">
<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>
{loading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
<div className="flex items-center justify-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<span className="text-muted-foreground">...</span>
</div>
</TableCell>
</TableRow>
) : 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<Application, ApplicationQuery>
ref={tableRef}
fetchFn={fetchData}
columns={columns}
searchFields={searchFields}
toolbar={toolbar}
rowKey="id"
minWidth="1400px"
/>
</CardContent>
</Card>