重构消息通知弹窗
This commit is contained in:
parent
7a4f5f37c2
commit
6de57aff67
@ -77,7 +77,16 @@ const EnvironmentList: React.FC = () => {
|
|||||||
{ key: 'id', title: 'ID', dataIndex: 'id', width: '80px' },
|
{ key: 'id', title: 'ID', dataIndex: 'id', width: '80px' },
|
||||||
{ key: 'envCode', title: '环境编码', dataIndex: 'envCode', width: '150px' },
|
{ key: 'envCode', title: '环境编码', dataIndex: 'envCode', width: '150px' },
|
||||||
{ key: 'envName', title: '环境名称', dataIndex: 'envName', 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: 'teamCount', title: '团队数', dataIndex: 'teamCount', width: '100px' },
|
||||||
{ key: 'applicationCount', title: '应用数', dataIndex: 'applicationCount', width: '100px' },
|
{ key: 'applicationCount', title: '应用数', dataIndex: 'applicationCount', width: '100px' },
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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 { PageContainer } from '@/components/ui/page-container';
|
||||||
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 { Separator } from '@/components/ui/separator';
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/components/ui/table';
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -24,33 +10,21 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { DataTablePagination } from '@/components/ui/pagination';
|
import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
|
||||||
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
|
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
|
||||||
Loader2,
|
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
TestTube,
|
TestTube,
|
||||||
RefreshCw,
|
|
||||||
Power,
|
Power,
|
||||||
PowerOff,
|
PowerOff,
|
||||||
Activity,
|
Activity,
|
||||||
Database,
|
Database,
|
||||||
Server,
|
Server,
|
||||||
MoreHorizontal,
|
Loader2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
import type {
|
import type {
|
||||||
NotificationChannel,
|
NotificationChannel,
|
||||||
NotificationChannelQuery,
|
NotificationChannelQuery,
|
||||||
@ -69,32 +43,16 @@ import NotificationChannelDialog from './components/NotificationChannelDialog';
|
|||||||
|
|
||||||
const NotificationChannelList: React.FC = () => {
|
const NotificationChannelList: React.FC = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const tableRef = useRef<PaginatedTableRef<NotificationChannel>>(null);
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [channels, setChannels] = useState<NotificationChannel[]>([]);
|
|
||||||
const [channelTypes, setChannelTypes] = useState<ChannelTypeOption[]>([]);
|
const [channelTypes, setChannelTypes] = useState<ChannelTypeOption[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [testing, setTesting] = useState<number | null>(null);
|
const [testing, setTesting] = useState<number | null>(null);
|
||||||
|
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
|
||||||
// 分页
|
|
||||||
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 [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [editId, setEditId] = useState<number | undefined>();
|
const [editId, setEditId] = useState<number | undefined>();
|
||||||
|
|
||||||
// 删除确认对话框
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||||
|
|
||||||
@ -103,11 +61,6 @@ const NotificationChannelList: React.FC = () => {
|
|||||||
loadChannelTypes();
|
loadChannelTypes();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 加载数据
|
|
||||||
useEffect(() => {
|
|
||||||
loadData();
|
|
||||||
}, [pagination.pageNum, pagination.pageSize, query.channelType, query.enabled]);
|
|
||||||
|
|
||||||
const loadChannelTypes = async () => {
|
const loadChannelTypes = async () => {
|
||||||
try {
|
try {
|
||||||
const types = await getChannelTypes();
|
const types = await getChannelTypes();
|
||||||
@ -121,69 +74,39 @@ const NotificationChannelList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadData = async () => {
|
// 包装 fetchFn,同时更新统计数据
|
||||||
setLoading(true);
|
const fetchData = async (query: any) => {
|
||||||
try {
|
const result = await getChannelsPage({
|
||||||
const res = await getChannelsPage({
|
|
||||||
...query,
|
...query,
|
||||||
page: pagination.pageNum,
|
page: query.pageNum,
|
||||||
size: pagination.pageSize,
|
size: query.pageSize,
|
||||||
});
|
});
|
||||||
|
// 更新统计
|
||||||
setChannels(res.content || []);
|
const all = result.content || [];
|
||||||
setPagination((prev) => ({
|
setStats({
|
||||||
...prev,
|
total: result.totalElements || 0,
|
||||||
totalElements: res.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,
|
|
||||||
});
|
});
|
||||||
} finally {
|
return result;
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 搜索
|
const handleAdd = () => {
|
||||||
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 = () => {
|
|
||||||
setEditId(undefined);
|
setEditId(undefined);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 编辑
|
|
||||||
const handleEdit = (id: number) => {
|
const handleEdit = (id: number) => {
|
||||||
setEditId(id);
|
setEditId(id);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除
|
const handleSuccess = () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setEditId(undefined);
|
||||||
|
tableRef.current?.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!deleteId) return;
|
if (!deleteId) return;
|
||||||
await deleteChannel(deleteId);
|
await deleteChannel(deleteId);
|
||||||
@ -199,7 +122,7 @@ const NotificationChannelList: React.FC = () => {
|
|||||||
await disableChannel(id);
|
await disableChannel(id);
|
||||||
toast({ title: '已禁用' });
|
toast({ title: '已禁用' });
|
||||||
}
|
}
|
||||||
loadData();
|
tableRef.current?.refresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
@ -253,25 +176,134 @@ const NotificationChannelList: React.FC = () => {
|
|||||||
return <Badge variant={config.variant}>{config.label}</Badge>;
|
return <Badge variant={config.variant}>{config.label}</Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 状态标签
|
// 搜索字段定义
|
||||||
const getStatusBadge = (enabled: boolean) => {
|
const searchFields: SearchFieldDef[] = useMemo(() => [
|
||||||
if (enabled) {
|
{ key: 'name', type: 'input', placeholder: '搜索渠道名称', width: 'w-[200px]' },
|
||||||
return <Badge variant="default">启用</Badge>;
|
{
|
||||||
}
|
key: 'channelType',
|
||||||
return <Badge variant="secondary">禁用</Badge>;
|
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 (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
<div className="grid gap-4 md:grid-cols-3 mb-6">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">总渠道数</CardTitle>
|
<CardTitle className="text-sm font-medium">总渠道数</CardTitle>
|
||||||
<Server className="h-4 w-4 text-muted-foreground" />
|
<Server className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<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>
|
<p className="text-xs text-muted-foreground">所有渠道</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -281,9 +313,7 @@ const NotificationChannelList: React.FC = () => {
|
|||||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-green-600">
|
<div className="text-2xl font-bold text-green-600">{stats.enabled}</div>
|
||||||
{channels.filter(c => c.enabled).length}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">正在使用中</p>
|
<p className="text-xs text-muted-foreground">正在使用中</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -293,9 +323,7 @@ const NotificationChannelList: React.FC = () => {
|
|||||||
<Database className="h-4 w-4 text-muted-foreground" />
|
<Database className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-gray-600">
|
<div className="text-2xl font-bold text-gray-600">{stats.disabled}</div>
|
||||||
{channels.filter(c => !c.enabled).length}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">暂时停用</p>
|
<p className="text-xs text-muted-foreground">暂时停用</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -304,178 +332,24 @@ const NotificationChannelList: 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>
|
||||||
<Button onClick={handleCreate}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
新建渠道
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<Separator />
|
||||||
{/* 搜索区域 */}
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<PaginatedTable<NotificationChannel, any>
|
||||||
<div className="flex-1">
|
ref={tableRef}
|
||||||
<Input
|
fetchFn={fetchData}
|
||||||
placeholder="搜索渠道名称"
|
columns={columns}
|
||||||
value={query.name}
|
searchFields={searchFields}
|
||||||
onChange={(e) => setQuery({ ...query, name: e.target.value })}
|
toolbar={toolbar}
|
||||||
onKeyPress={(e) => {
|
rowKey="id"
|
||||||
if (e.key === 'Enter') {
|
minWidth="1200px"
|
||||||
handleSearch();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -485,7 +359,7 @@ const NotificationChannelList: React.FC = () => {
|
|||||||
editId={editId}
|
editId={editId}
|
||||||
channelTypes={channelTypes}
|
channelTypes={channelTypes}
|
||||||
onOpenChange={setDialogOpen}
|
onOpenChange={setDialogOpen}
|
||||||
onSuccess={loadData}
|
onSuccess={handleSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 删除确认对话框 */}
|
{/* 删除确认对话框 */}
|
||||||
@ -499,7 +373,7 @@ const NotificationChannelList: React.FC = () => {
|
|||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
toast({ title: '删除成功' });
|
toast({ title: '删除成功' });
|
||||||
setDeleteId(null);
|
setDeleteId(null);
|
||||||
loadData();
|
tableRef.current?.refresh();
|
||||||
}}
|
}}
|
||||||
onCancel={() => setDeleteId(null)}
|
onCancel={() => setDeleteId(null)}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
|||||||
@ -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 { PageContainer } from '@/components/ui/page-container';
|
||||||
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 { Separator } from '@/components/ui/separator';
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/components/ui/table';
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -27,15 +13,11 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { DataTablePagination } from '@/components/ui/pagination';
|
|
||||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
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 {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
|
||||||
Loader2,
|
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
TestTube,
|
TestTube,
|
||||||
@ -44,15 +26,8 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
Database,
|
Database,
|
||||||
Server,
|
Server,
|
||||||
MoreHorizontal
|
Loader2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
import { NotificationChannelType } from '../../NotificationChannel/List/types';
|
import { NotificationChannelType } from '../../NotificationChannel/List/types';
|
||||||
import type {
|
import type {
|
||||||
NotificationTemplateDTO,
|
NotificationTemplateDTO,
|
||||||
@ -83,41 +58,19 @@ const CHANNEL_TYPE_OPTIONS: ChannelTypeOption[] = [
|
|||||||
|
|
||||||
const NotificationTemplateList: React.FC = () => {
|
const NotificationTemplateList: React.FC = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const tableRef = useRef<PaginatedTableRef<NotificationTemplateDTO>>(null);
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [loading, setLoading] = useState(false);
|
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
|
||||||
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 [operationLoading, setOperationLoading] = useState({
|
const [operationLoading, setOperationLoading] = useState({
|
||||||
toggling: null as number | null,
|
toggling: null as number | null,
|
||||||
deleting: null as number | null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 查询条件
|
|
||||||
const [query, setQuery] = useState<NotificationTemplateQuery>({});
|
|
||||||
|
|
||||||
// 对话框状态
|
// 对话框状态
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [currentTemplate, setCurrentTemplate] = useState<NotificationTemplateDTO | undefined>();
|
const [currentTemplate, setCurrentTemplate] = useState<NotificationTemplateDTO | undefined>();
|
||||||
|
|
||||||
// 测试模板对话框状态
|
|
||||||
const [testModalVisible, setTestModalVisible] = useState(false);
|
const [testModalVisible, setTestModalVisible] = useState(false);
|
||||||
const [testTemplate, setTestTemplate] = useState<NotificationTemplateDTO | undefined>();
|
const [testTemplate, setTestTemplate] = useState<NotificationTemplateDTO | undefined>();
|
||||||
|
|
||||||
// 确认对话框
|
|
||||||
const [confirmDialog, setConfirmDialog] = useState<{
|
const [confirmDialog, setConfirmDialog] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
@ -130,73 +83,35 @@ const NotificationTemplateList: React.FC = () => {
|
|||||||
onConfirm: async () => {},
|
onConfirm: async () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载数据
|
// 包装 fetchFn,同时更新统计数据
|
||||||
const loadData = async (params?: NotificationTemplateQuery) => {
|
const fetchData = async (query: NotificationTemplateQuery) => {
|
||||||
setLoading(true);
|
const result = await getTemplateList(query);
|
||||||
try {
|
// 更新统计
|
||||||
const queryParams: NotificationTemplateQuery = {
|
const all = result.content || [];
|
||||||
...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 || [];
|
|
||||||
setStats({
|
setStats({
|
||||||
total: all.length,
|
total: result.totalElements || 0,
|
||||||
enabled: all.filter((item) => item.enabled).length,
|
enabled: all.filter((item) => item.enabled).length,
|
||||||
disabled: all.filter((item) => !item.enabled).length,
|
disabled: all.filter((item) => !item.enabled).length,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
return result;
|
||||||
toast({
|
|
||||||
variant: 'destructive',
|
|
||||||
title: '获取模板列表失败',
|
|
||||||
description: error.response?.data?.message || error.message,
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 分页处理
|
|
||||||
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 = () => {
|
const handleAdd = () => {
|
||||||
setCurrentTemplate(undefined);
|
setCurrentTemplate(undefined);
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 编辑模板
|
|
||||||
const handleEdit = (template: NotificationTemplateDTO) => {
|
const handleEdit = (template: NotificationTemplateDTO) => {
|
||||||
setCurrentTemplate(template);
|
setCurrentTemplate(template);
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSuccess = () => {
|
||||||
|
setModalVisible(false);
|
||||||
|
setCurrentTemplate(undefined);
|
||||||
|
tableRef.current?.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
// 删除模板
|
// 删除模板
|
||||||
const handleDelete = (template: NotificationTemplateDTO) => {
|
const handleDelete = (template: NotificationTemplateDTO) => {
|
||||||
setConfirmDialog({
|
setConfirmDialog({
|
||||||
@ -207,7 +122,7 @@ const NotificationTemplateList: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await deleteTemplate(template.id);
|
await deleteTemplate(template.id);
|
||||||
toast({ title: '删除成功' });
|
toast({ title: '删除成功' });
|
||||||
loadData();
|
tableRef.current?.refresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
@ -237,7 +152,7 @@ const NotificationTemplateList: React.FC = () => {
|
|||||||
await enableTemplate(template.id);
|
await enableTemplate(template.id);
|
||||||
toast({ title: '已启用' });
|
toast({ title: '已启用' });
|
||||||
}
|
}
|
||||||
loadData();
|
tableRef.current?.refresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
@ -255,10 +170,136 @@ const NotificationTemplateList: React.FC = () => {
|
|||||||
return option?.label || type;
|
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 (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
<div className="grid gap-4 md:grid-cols-3 mb-6">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">总模板数</CardTitle>
|
<CardTitle className="text-sm font-medium">总模板数</CardTitle>
|
||||||
@ -294,184 +335,24 @@ const NotificationTemplateList: 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">
|
||||||
管理通知消息模板,支持 FreeMarker 语法
|
管理通知消息模板,支持 FreeMarker 语法
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleAdd}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
新建模板
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<Separator />
|
||||||
{/* 搜索区域 */}
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<PaginatedTable<NotificationTemplateDTO, NotificationTemplateQuery>
|
||||||
<div className="flex-1">
|
ref={tableRef}
|
||||||
<Input
|
fetchFn={fetchData}
|
||||||
placeholder="搜索模板名称或编码"
|
columns={columns}
|
||||||
value={query.name || ''}
|
searchFields={searchFields}
|
||||||
onChange={(e) => setQuery(prev => ({ ...prev, name: e.target.value }))}
|
toolbar={toolbar}
|
||||||
onKeyPress={(e) => {
|
rowKey="id"
|
||||||
if (e.key === 'Enter') {
|
minWidth="1300px"
|
||||||
handleSearch();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -481,7 +362,7 @@ const NotificationTemplateList: React.FC = () => {
|
|||||||
editId={currentTemplate?.id}
|
editId={currentTemplate?.id}
|
||||||
channelTypes={CHANNEL_TYPE_OPTIONS}
|
channelTypes={CHANNEL_TYPE_OPTIONS}
|
||||||
onOpenChange={setModalVisible}
|
onOpenChange={setModalVisible}
|
||||||
onSuccess={loadData}
|
onSuccess={handleSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 测试模板对话框 */}
|
{/* 测试模板对话框 */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user