重构消息通知弹窗

This commit is contained in:
dengqichen 2025-11-28 16:57:57 +08:00
parent 7a4f5f37c2
commit 6de57aff67
3 changed files with 345 additions and 581 deletions

View File

@ -77,7 +77,16 @@ const EnvironmentList: React.FC = () => {
{ key: 'id', title: 'ID', dataIndex: 'id', width: '80px' },
{ key: 'envCode', title: '环境编码', dataIndex: 'envCode', width: '150px' },
{ key: 'envName', title: '环境名称', dataIndex: 'envName', width: '150px' },
{ key: 'envDesc', title: '环境描述', dataIndex: 'envDesc', width: '200px' },
{
key: 'envDesc',
title: '环境描述',
width: '200px',
render: (_, record) => (
<div className="truncate max-w-[200px]" title={record.envDesc || ''}>
{record.envDesc || '-'}
</div>
),
},
{ key: 'teamCount', title: '团队数', dataIndex: 'teamCount', width: '100px' },
{ key: 'applicationCount', title: '应用数', dataIndex: 'applicationCount', width: '100px' },
{

View File

@ -1,22 +1,8 @@
import React, { useEffect, useState } from 'react';
import React, { useState, useMemo, useRef, useEffect } from 'react';
import { PageContainer } from '@/components/ui/page-container';
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,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import {
Card,
CardContent,
@ -24,33 +10,21 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { useToast } from '@/components/ui/use-toast';
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
import {
Plus,
Search,
Loader2,
Edit,
Trash2,
TestTube,
RefreshCw,
Power,
PowerOff,
Activity,
Database,
Server,
MoreHorizontal,
Loader2,
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import type {
NotificationChannel,
NotificationChannelQuery,
@ -69,32 +43,16 @@ import NotificationChannelDialog from './components/NotificationChannelDialog';
const NotificationChannelList: React.FC = () => {
const { toast } = useToast();
const tableRef = useRef<PaginatedTableRef<NotificationChannel>>(null);
// 状态管理
const [channels, setChannels] = useState<NotificationChannel[]>([]);
const [channelTypes, setChannelTypes] = useState<ChannelTypeOption[]>([]);
const [loading, setLoading] = useState(false);
const [testing, setTesting] = useState<number | null>(null);
// 分页
const [pagination, setPagination] = useState({
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
// 查询条件
const [query, setQuery] = useState<NotificationChannelQuery>({
name: '',
channelType: undefined,
enabled: undefined,
});
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
// 对话框状态
const [dialogOpen, setDialogOpen] = useState(false);
const [editId, setEditId] = useState<number | undefined>();
// 删除确认对话框
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null);
@ -103,11 +61,6 @@ const NotificationChannelList: React.FC = () => {
loadChannelTypes();
}, []);
// 加载数据
useEffect(() => {
loadData();
}, [pagination.pageNum, pagination.pageSize, query.channelType, query.enabled]);
const loadChannelTypes = async () => {
try {
const types = await getChannelTypes();
@ -121,69 +74,39 @@ const NotificationChannelList: React.FC = () => {
}
};
const loadData = async () => {
setLoading(true);
try {
const res = await getChannelsPage({
// 包装 fetchFn同时更新统计数据
const fetchData = async (query: any) => {
const result = await getChannelsPage({
...query,
page: pagination.pageNum,
size: pagination.pageSize,
page: query.pageNum,
size: query.pageSize,
});
setChannels(res.content || []);
setPagination((prev) => ({
...prev,
totalElements: res.totalElements || 0,
}));
} catch (error: any) {
toast({
variant: 'destructive',
title: '加载失败',
description: error.response?.data?.message || error.message,
// 更新统计
const all = result.content || [];
setStats({
total: result.totalElements || 0,
enabled: all.filter((item) => item.enabled).length,
disabled: all.filter((item) => !item.enabled).length,
});
} finally {
setLoading(false);
}
return result;
};
// 搜索
const handleSearch = () => {
setPagination((prev) => ({ ...prev, pageNum: DEFAULT_PAGE_NUM }));
loadData();
};
// 重置搜索
const handleReset = () => {
setQuery({
name: '',
channelType: undefined,
enabled: undefined,
});
setPagination({
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
};
// 分页切换
const handlePageChange = (newPage: number) => {
setPagination({ ...pagination, pageNum: newPage });
};
// 创建
const handleCreate = () => {
const handleAdd = () => {
setEditId(undefined);
setDialogOpen(true);
};
// 编辑
const handleEdit = (id: number) => {
setEditId(id);
setDialogOpen(true);
};
// 删除
const handleSuccess = () => {
setDialogOpen(false);
setEditId(undefined);
tableRef.current?.refresh();
};
const handleDelete = async () => {
if (!deleteId) return;
await deleteChannel(deleteId);
@ -199,7 +122,7 @@ const NotificationChannelList: React.FC = () => {
await disableChannel(id);
toast({ title: '已禁用' });
}
loadData();
tableRef.current?.refresh();
} catch (error: any) {
toast({
variant: 'destructive',
@ -253,25 +176,134 @@ const NotificationChannelList: React.FC = () => {
return <Badge variant={config.variant}>{config.label}</Badge>;
};
// 状态标签
const getStatusBadge = (enabled: boolean) => {
if (enabled) {
return <Badge variant="default"></Badge>;
}
return <Badge variant="secondary"></Badge>;
};
// 搜索字段定义
const searchFields: SearchFieldDef[] = useMemo(() => [
{ key: 'name', type: 'input', placeholder: '搜索渠道名称', width: 'w-[200px]' },
{
key: 'channelType',
type: 'select',
placeholder: '渠道类型',
width: 'w-[150px]',
options: channelTypes
.filter((type) => type.code && type.code.trim() !== '')
.map((type) => ({ label: type.label, value: type.code })),
},
], [channelTypes]);
// 列定义
const columns: ColumnDef<NotificationChannel>[] = useMemo(() => [
{ key: 'id', title: 'ID', dataIndex: 'id', width: '80px' },
{ key: 'name', title: '渠道名称', dataIndex: 'name', width: '150px' },
{
key: 'channelType',
title: '渠道类型',
width: '120px',
render: (_, record) => getChannelTypeBadge(record.channelType),
},
{
key: 'enabled',
title: '状态',
width: '100px',
render: (_, record) => (
<Badge variant={record.enabled ? 'default' : 'secondary'}>
{record.enabled ? '启用' : '禁用'}
</Badge>
),
},
{
key: 'description',
title: '描述',
width: '200px',
render: (_, record) => (
<div className="truncate max-w-[200px]" title={record.description || ''}>
{record.description || '-'}
</div>
),
},
{ key: 'createTime', title: '创建时间', dataIndex: 'createTime', width: '180px' },
{
key: 'actions',
title: '操作',
width: '240px',
sticky: true,
render: (_, record) => (
<div className="flex items-center gap-2">
<Button
size="sm"
variant="ghost"
onClick={() => handleTest(record.id!)}
disabled={testing === record.id}
title="测试连接"
>
{testing === record.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<TestTube className="h-4 w-4" />
)}
</Button>
{record.enabled ? (
<Button
size="sm"
variant="ghost"
onClick={() => handleToggleStatus(record.id!, false)}
title="禁用"
>
<PowerOff className="h-4 w-4" />
</Button>
) : (
<Button
size="sm"
variant="ghost"
onClick={() => handleToggleStatus(record.id!, true)}
title="启用"
>
<Power className="h-4 w-4" />
</Button>
)}
<Button
size="sm"
variant="ghost"
onClick={() => handleEdit(record.id!)}
title="编辑"
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
setDeleteId(record.id!);
setDeleteDialogOpen(true);
}}
className="text-destructive hover:text-destructive"
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
),
},
], [testing, channelTypes]);
// 工具栏
const toolbar = (
<Button onClick={handleAdd}>
<Plus className="h-4 w-4 mr-2" />
</Button>
);
return (
<PageContainer>
{/* 统计卡片 */}
<div className="grid gap-4 md:grid-cols-3 mb-6">
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{channels.length}</div>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
@ -281,9 +313,7 @@ const NotificationChannelList: React.FC = () => {
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{channels.filter(c => c.enabled).length}
</div>
<div className="text-2xl font-bold text-green-600">{stats.enabled}</div>
<p className="text-xs text-muted-foreground">使</p>
</CardContent>
</Card>
@ -293,9 +323,7 @@ const NotificationChannelList: React.FC = () => {
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-600">
{channels.filter(c => !c.enabled).length}
</div>
<div className="text-2xl font-bold text-gray-600">{stats.disabled}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
@ -304,178 +332,24 @@ const NotificationChannelList: React.FC = () => {
{/* 渠道管理 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle></CardTitle>
<CardDescription className="mt-2">
</CardDescription>
</div>
<Button onClick={handleCreate}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
{/* 搜索区域 */}
<div className="flex items-center gap-4 mb-4">
<div className="flex-1">
<Input
placeholder="搜索渠道名称"
value={query.name}
onChange={(e) => setQuery({ ...query, name: e.target.value })}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSearch();
}
}}
<Separator />
<CardContent className="pt-6">
<PaginatedTable<NotificationChannel, any>
ref={tableRef}
fetchFn={fetchData}
columns={columns}
searchFields={searchFields}
toolbar={toolbar}
rowKey="id"
minWidth="1200px"
/>
</div>
<Select
value={query.channelType || 'all'}
onValueChange={(value) =>
setQuery({
...query,
channelType:
value === 'all' ? undefined : (value as NotificationChannelType),
})
}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="选择渠道类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{channelTypes
.filter((type) => type.code && type.code.trim() !== '')
.map((type) => (
<SelectItem key={type.code} value={type.code}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={handleSearch}>
<Search className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 表格 */}
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : channels.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
</TableCell>
</TableRow>
) : (
channels.map((channel) => (
<TableRow key={channel.id}>
<TableCell className="font-medium">{channel.name}</TableCell>
<TableCell>
{getChannelTypeBadge(channel.channelType)}
</TableCell>
<TableCell>
<Badge variant={channel.enabled ? "default" : "secondary"}>
{channel.enabled ? "启用" : "禁用"}
</Badge>
</TableCell>
<TableCell className="max-w-xs truncate">
{channel.description || '-'}
</TableCell>
<TableCell>{channel.createTime}</TableCell>
<TableCell className="text-right">
<div className="flex items-center gap-2 justify-end">
<Button
size="sm"
variant="outline"
onClick={() => handleTest(channel.id!)}
disabled={testing === channel.id}
title="测试连接"
>
{testing === channel.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<TestTube className="h-4 w-4" />
)}
</Button>
{channel.enabled ? (
<Button
size="sm"
variant="outline"
onClick={() => handleToggleStatus(channel.id!, false)}
title="禁用"
>
<PowerOff className="h-4 w-4" />
</Button>
) : (
<Button
size="sm"
variant="outline"
onClick={() => handleToggleStatus(channel.id!, true)}
title="启用"
>
<Power className="h-4 w-4" />
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => handleEdit(channel.id!)}
title="编辑"
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
setDeleteId(channel.id!);
setDeleteDialogOpen(true);
}}
title="删除"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
<div className="mt-4">
<DataTablePagination
currentPage={pagination.pageNum}
totalPages={Math.ceil(pagination.totalElements / pagination.pageSize)}
totalItems={pagination.totalElements}
pageSize={pagination.pageSize}
onPageChange={handlePageChange}
/>
</div>
</CardContent>
</Card>
@ -485,7 +359,7 @@ const NotificationChannelList: React.FC = () => {
editId={editId}
channelTypes={channelTypes}
onOpenChange={setDialogOpen}
onSuccess={loadData}
onSuccess={handleSuccess}
/>
{/* 删除确认对话框 */}
@ -499,7 +373,7 @@ const NotificationChannelList: React.FC = () => {
onSuccess={() => {
toast({ title: '删除成功' });
setDeleteId(null);
loadData();
tableRef.current?.refresh();
}}
onCancel={() => setDeleteId(null)}
variant="destructive"

View File

@ -1,25 +1,11 @@
/**
*
*/
import React, { useState, useEffect } from 'react';
import React, { useState, useMemo, useRef } from 'react';
import { PageContainer } from '@/components/ui/page-container';
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,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import {
Card,
CardContent,
@ -27,15 +13,11 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/components/ui/use-toast';
import { DataTablePagination } from '@/components/ui/pagination';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
import {
Plus,
Search,
Loader2,
Edit,
Trash2,
TestTube,
@ -44,15 +26,8 @@ import {
Activity,
Database,
Server,
MoreHorizontal
Loader2,
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { NotificationChannelType } from '../../NotificationChannel/List/types';
import type {
NotificationTemplateDTO,
@ -83,41 +58,19 @@ const CHANNEL_TYPE_OPTIONS: ChannelTypeOption[] = [
const NotificationTemplateList: React.FC = () => {
const { toast } = useToast();
const tableRef = useRef<PaginatedTableRef<NotificationTemplateDTO>>(null);
// 状态管理
const [loading, setLoading] = useState(false);
const [list, setList] = useState<NotificationTemplateDTO[]>([]);
const [pagination, setPagination] = useState({
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
// 统计数据
const [stats, setStats] = useState({
total: 0,
enabled: 0,
disabled: 0,
});
// 操作状态管理
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
const [operationLoading, setOperationLoading] = useState({
toggling: null as number | null,
deleting: null as number | null,
});
// 查询条件
const [query, setQuery] = useState<NotificationTemplateQuery>({});
// 对话框状态
const [modalVisible, setModalVisible] = useState(false);
const [currentTemplate, setCurrentTemplate] = useState<NotificationTemplateDTO | undefined>();
// 测试模板对话框状态
const [testModalVisible, setTestModalVisible] = useState(false);
const [testTemplate, setTestTemplate] = useState<NotificationTemplateDTO | undefined>();
// 确认对话框
const [confirmDialog, setConfirmDialog] = useState<{
open: boolean;
title: string;
@ -130,73 +83,35 @@ const NotificationTemplateList: React.FC = () => {
onConfirm: async () => {},
});
// 加载数据
const loadData = async (params?: NotificationTemplateQuery) => {
setLoading(true);
try {
const queryParams: NotificationTemplateQuery = {
...query,
...params,
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
};
const data = await getTemplateList(queryParams);
setList(data.content || []);
setPagination({
...pagination,
totalElements: data.totalElements,
});
// 计算统计数据
const all = data.content || [];
// 包装 fetchFn同时更新统计数据
const fetchData = async (query: NotificationTemplateQuery) => {
const result = await getTemplateList(query);
// 更新统计
const all = result.content || [];
setStats({
total: all.length,
total: result.totalElements || 0,
enabled: all.filter((item) => item.enabled).length,
disabled: all.filter((item) => !item.enabled).length,
});
} catch (error: any) {
toast({
variant: 'destructive',
title: '获取模板列表失败',
description: error.response?.data?.message || error.message,
duration: 3000,
});
} finally {
setLoading(false);
}
return result;
};
// 分页处理
const handlePageChange = (page: number) => {
setPagination({
...pagination,
pageNum: page,
});
};
// 初始加载
useEffect(() => {
loadData();
}, [pagination.pageNum, pagination.pageSize]);
// 搜索处理
const handleSearch = () => {
setPagination(prev => ({ ...prev, pageNum: DEFAULT_PAGE_NUM }));
loadData();
};
// 新建模板
const handleAdd = () => {
setCurrentTemplate(undefined);
setModalVisible(true);
};
// 编辑模板
const handleEdit = (template: NotificationTemplateDTO) => {
setCurrentTemplate(template);
setModalVisible(true);
};
const handleSuccess = () => {
setModalVisible(false);
setCurrentTemplate(undefined);
tableRef.current?.refresh();
};
// 删除模板
const handleDelete = (template: NotificationTemplateDTO) => {
setConfirmDialog({
@ -207,7 +122,7 @@ const NotificationTemplateList: React.FC = () => {
try {
await deleteTemplate(template.id);
toast({ title: '删除成功' });
loadData();
tableRef.current?.refresh();
} catch (error: any) {
toast({
variant: 'destructive',
@ -237,7 +152,7 @@ const NotificationTemplateList: React.FC = () => {
await enableTemplate(template.id);
toast({ title: '已启用' });
}
loadData();
tableRef.current?.refresh();
} catch (error: any) {
toast({
variant: 'destructive',
@ -255,10 +170,136 @@ const NotificationTemplateList: React.FC = () => {
return option?.label || type;
};
// 搜索字段定义
const searchFields: SearchFieldDef[] = useMemo(() => [
{ key: 'name', type: 'input', placeholder: '搜索模板名称或编码', width: 'w-[220px]' },
{
key: 'channelType',
type: 'select',
placeholder: '渠道类型',
width: 'w-[150px]',
options: CHANNEL_TYPE_OPTIONS.map((option) => ({
label: option.label,
value: option.code,
})),
},
], []);
// 列定义
const columns: ColumnDef<NotificationTemplateDTO>[] = useMemo(() => [
{ key: 'id', title: 'ID', dataIndex: 'id', width: '80px' },
{ key: 'name', title: '模板名称', dataIndex: 'name', width: '150px' },
{ key: 'code', title: '模板编码', dataIndex: 'code', width: '150px' },
{
key: 'titleTemplate',
title: '标题模板',
width: '200px',
render: (_, record) => (
<div className="truncate max-w-[200px]" title={record.titleTemplate || ''}>
{record.titleTemplate || '-'}
</div>
),
},
{
key: 'channelType',
title: '渠道类型',
width: '120px',
render: (_, record) => (
<Badge variant="outline">{getChannelTypeLabel(record.channelType)}</Badge>
),
},
{
key: 'enabled',
title: '状态',
width: '100px',
render: (_, record) => (
<Badge variant={record.enabled ? 'default' : 'secondary'}>
{record.enabled ? '启用' : '禁用'}
</Badge>
),
},
{ key: 'createTime', title: '创建时间', dataIndex: 'createTime', width: '180px' },
{
key: 'actions',
title: '操作',
width: '240px',
sticky: true,
render: (_, record) => (
<div className="flex items-center gap-2">
<Button
size="sm"
variant="ghost"
onClick={() => handleTestTemplate(record)}
disabled={!record.enabled}
title="测试模板"
>
<TestTube className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleEdit(record)}
disabled={operationLoading.toggling === record.id}
title="编辑"
>
<Edit className="h-4 w-4" />
</Button>
{record.enabled ? (
<Button
size="sm"
variant="ghost"
onClick={() => handleToggleStatus(record)}
disabled={operationLoading.toggling === record.id}
title="禁用"
>
{operationLoading.toggling === record.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<PowerOff className="h-4 w-4" />
)}
</Button>
) : (
<Button
size="sm"
variant="ghost"
onClick={() => handleToggleStatus(record)}
disabled={operationLoading.toggling === record.id}
title="启用"
>
{operationLoading.toggling === record.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Power className="h-4 w-4" />
)}
</Button>
)}
<Button
size="sm"
variant="ghost"
onClick={() => handleDelete(record)}
disabled={operationLoading.toggling === record.id}
className="text-destructive hover:text-destructive"
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
),
},
], [operationLoading.toggling]);
// 工具栏
const toolbar = (
<Button onClick={handleAdd}>
<Plus className="h-4 w-4 mr-2" />
</Button>
);
return (
<PageContainer>
{/* 统计卡片 */}
<div className="grid gap-4 md:grid-cols-3 mb-6">
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
@ -294,184 +335,24 @@ const NotificationTemplateList: React.FC = () => {
{/* 模板管理 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle></CardTitle>
<CardDescription className="mt-2">
FreeMarker
</CardDescription>
</div>
<Button onClick={handleAdd}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
{/* 搜索区域 */}
<div className="flex items-center gap-4 mb-4">
<div className="flex-1">
<Input
placeholder="搜索模板名称或编码"
value={query.name || ''}
onChange={(e) => setQuery(prev => ({ ...prev, name: e.target.value }))}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSearch();
}
}}
<Separator />
<CardContent className="pt-6">
<PaginatedTable<NotificationTemplateDTO, NotificationTemplateQuery>
ref={tableRef}
fetchFn={fetchData}
columns={columns}
searchFields={searchFields}
toolbar={toolbar}
rowKey="id"
minWidth="1300px"
/>
</div>
<Select
value={query.channelType || 'all'}
onValueChange={(value) =>
setQuery(prev => ({
...prev,
channelType: value === 'all' ? undefined : value as NotificationChannelType
}))
}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="选择渠道类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{CHANNEL_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.code} value={option.code}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={handleSearch}>
<Search className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 表格 */}
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={7} className="h-32 text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : list.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="h-32 text-center text-muted-foreground">
</TableCell>
</TableRow>
) : (
list.map((template) => (
<TableRow key={template.id}>
<TableCell className="font-medium">{template.name}</TableCell>
<TableCell>{template.code}</TableCell>
<TableCell className="max-w-xs truncate" title={template.titleTemplate}>
{template.titleTemplate || '-'}
</TableCell>
<TableCell>
<Badge variant="outline">
{getChannelTypeLabel(template.channelType)}
</Badge>
</TableCell>
<TableCell>
<Badge variant={template.enabled ? "default" : "secondary"}>
{template.enabled ? "启用" : "禁用"}
</Badge>
</TableCell>
<TableCell>{template.createTime}</TableCell>
<TableCell className="text-right">
<div className="flex items-center gap-2 justify-end">
<Button
size="sm"
variant="outline"
onClick={() => handleTestTemplate(template)}
disabled={!template.enabled}
title="测试模板"
>
<TestTube className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleEdit(template)}
disabled={operationLoading.toggling === template.id || operationLoading.deleting === template.id}
title="编辑"
>
<Edit className="h-4 w-4" />
</Button>
{template.enabled ? (
<Button
size="sm"
variant="outline"
onClick={() => handleToggleStatus(template)}
disabled={operationLoading.toggling === template.id}
title="禁用"
>
{operationLoading.toggling === template.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<PowerOff className="h-4 w-4" />
)}
</Button>
) : (
<Button
size="sm"
variant="outline"
onClick={() => handleToggleStatus(template)}
disabled={operationLoading.toggling === template.id}
title="启用"
>
{operationLoading.toggling === template.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Power className="h-4 w-4" />
)}
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => handleDelete(template)}
disabled={operationLoading.toggling === template.id || operationLoading.deleting === template.id}
title="删除"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
<div className="mt-4">
<DataTablePagination
currentPage={pagination.pageNum}
totalPages={Math.ceil(pagination.totalElements / pagination.pageSize)}
totalItems={pagination.totalElements}
pageSize={pagination.pageSize}
onPageChange={handlePageChange}
/>
</div>
</CardContent>
</Card>
@ -481,7 +362,7 @@ const NotificationTemplateList: React.FC = () => {
editId={currentTemplate?.id}
channelTypes={CHANNEL_TYPE_OPTIONS}
onOpenChange={setModalVisible}
onSuccess={loadData}
onSuccess={handleSuccess}
/>
{/* 测试模板对话框 */}