// Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 'use client'; import * as React from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { ChevronDown, Edit2, Layers, Loader2, Plus, Trash2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { EmptyStateWithBorder } from '@/components/layout/empty'; import services from '@/lib/services'; import type { CreatePushEventRequest, PushEvent, UpdatePushEventRequest, } from '@/lib/services/push'; import { PushService } from '@/lib/services/push'; export function EventsTab() { const queryClient = useQueryClient(); // --- 获取所有自定义消息通道 --- const channelsQuery = useQuery({ queryKey: ['admin', 'push-channels'], queryFn: () => PushService.listChannels(), }); const availableChannels = React.useMemo(() => { const customChannels = (channelsQuery.data ?? []) .filter((c) => c.enabled) .map((c) => c.name); return ['email', ...customChannels]; }, [channelsQuery.data]); // --- 获取通知事件 --- const eventsQuery = useQuery({ queryKey: ['admin', 'push-events'], queryFn: () => PushService.listEvents(), }); const builtInEventsQuery = useQuery({ queryKey: ['admin', 'push-builtin-events'], queryFn: () => PushService.listBuiltInEvents(), }); // --- 获取所有系统可调度任务类型 --- const taskTypesQuery = useQuery({ queryKey: ['admin', 'task-types'], queryFn: () => services.adminTask.getTaskTypes(), }); // --- 修改保存事件 Mutation --- const updateEventMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: UpdatePushEventRequest }) => PushService.updateEvent(id, data), onSuccess: () => { toast.success('事件更新成功'); queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); setEditEventOpen(false); }, onError: (err: unknown) => { toast.error('事件更新失败: ' + (err as Error).message); }, }); const toggleEventMutation = useMutation({ mutationFn: (id: number) => PushService.toggleEvent(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); }, onError: (err: unknown) => { toast.error('操作失败: ' + (err as Error).message); }, }); const createEventMutation = useMutation({ mutationFn: (data: CreatePushEventRequest) => PushService.createEvent(data), onSuccess: () => { toast.success('事件创建成功'); queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); setCreateEventOpen(false); setNewEventKey(''); setNewEventType('builtin'); setNewEventTaskType(''); setNewEventChannels([]); setNewEventEnabled(true); }, onError: (err: unknown) => { toast.error('事件创建失败: ' + (err as Error).message); }, }); const deleteEventMutation = useMutation({ mutationFn: (id: number) => PushService.deleteEvent(id), onSuccess: () => { toast.success('配置删除成功'); queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); }, onError: (err: unknown) => { toast.error('配置删除失败: ' + (err as Error).message); }, }); // --- 事件编辑对话框状态 --- const [editEventOpen, setEditEventOpen] = React.useState(false); const [selectedEvent, setSelectedEvent] = React.useState( null, ); const [eventChannels, setEventChannels] = React.useState([]); const [eventTargets, setEventTargets] = React.useState(''); const [eventTemplate, setEventTemplate] = React.useState(''); // 事件创建对话框状态 const [createEventOpen, setCreateEventOpen] = React.useState(false); const [newEventType, setNewEventType] = React.useState<'builtin' | 'task'>( 'builtin', ); const [newEventKey, setNewEventKey] = React.useState(''); const [newEventTaskType, setNewEventTaskType] = React.useState(''); const [newEventChannels, setNewEventChannels] = React.useState([]); const [newEventTargets, setNewEventTargets] = React.useState(''); const [newEventTemplate, setNewEventTemplate] = React.useState(''); const [newEventEnabled, setNewEventEnabled] = React.useState(true); const availableBuiltInEvents = React.useMemo(() => { const configuredKeys = new Set( (eventsQuery.data ?? []).map((e) => e.event_key), ); return (builtInEventsQuery.data ?? []).filter( (e) => !configuredKeys.has(e.key), ); }, [builtInEventsQuery.data, eventsQuery.data]); const handleEditEventClick = (event: PushEvent) => { setSelectedEvent(event); setEventChannels(event.channels); setEventTargets((event.targets ?? []).join(', ')); setEventTemplate(event.template); setEditEventOpen(true); }; const handleSaveEvent = () => { if (!selectedEvent) return; try { JSON.parse(eventTemplate); } catch { toast.error('消息模板不是合法的 JSON 格式'); return; } const targets = eventTargets .split(',') .map((t) => t.trim()) .filter((t) => t !== ''); updateEventMutation.mutate({ id: selectedEvent.id, data: { channels: eventChannels, targets, template: eventTemplate, enabled: selectedEvent.enabled, }, }); }; const handleCreateEventClick = () => { setNewEventKey(''); setNewEventType('builtin'); setNewEventTaskType(''); setNewEventChannels([]); setNewEventTargets(''); setNewEventTemplate(''); setNewEventEnabled(true); setCreateEventOpen(true); }; const handleNewEventKeyChange = (key: string) => { setNewEventKey(key); const ev = availableBuiltInEvents.find((e) => e.key === key); if (ev) { setNewEventTemplate(JSON.stringify(ev.default_template, null, 2)); } else { setNewEventTemplate(''); } }; const handleNewEventTaskTypeChange = (taskType: string) => { setNewEventTaskType(taskType); const taskMeta = (taskTypesQuery.data ?? []).find( (t) => t.asynq_task === taskType, ); if (taskMeta) { const defaultTemplate = { title: `任务完成: ${taskMeta.name}`, content: `异步任务 {{task_name}} 已完成。状态: {{task_status}},耗时: {{task_duration}} ms。`, level: 'INFO', }; setNewEventTemplate(JSON.stringify(defaultTemplate, null, 2)); } else { setNewEventTemplate(''); } }; const handleCreateEvent = () => { if (newEventType === 'builtin' && !newEventKey) { toast.error('请选择系统事件'); return; } if (newEventType === 'task' && !newEventTaskType) { toast.error('请选择异步任务'); return; } if (newEventTemplate) { try { JSON.parse(newEventTemplate); } catch { toast.error('内容渲染模板不是合法的 JSON 格式'); return; } } const targets = newEventTargets .split(',') .map((t) => t.trim()) .filter((t) => t !== ''); createEventMutation.mutate({ event_key: newEventType === 'builtin' ? newEventKey : undefined, task_type: newEventType === 'task' ? newEventTaskType : undefined, channels: newEventChannels, targets: targets.length > 0 ? targets : undefined, template: newEventTemplate || undefined, enabled: newEventEnabled, }); }; return (
{eventsQuery.isLoading ? ( ) : eventsQuery.isError ? (
eventsQuery.refetch()} className='justify-center' />
) : (eventsQuery.data ?? []).length === 0 ? ( ) : (
ID 通知事件 关联渠道 推送目标 状态 操作 {(eventsQuery.data ?? []).map((event) => ( handleEditEventClick(event)} > {event.id}
{event.name} {event.task_type && ( 任务 )}
{event.event_key}
{(event.channels ?? []).length === 0 ? ( 未指定渠道 ) : ( event.channels.map((ch) => ( {ch === 'email' ? '邮件' : ch} )) )}
{!event.targets || event.targets.length === 0 ? ( - ) : ( event.targets.map((t) => ( {t} )) )}
e.stopPropagation()} > toggleEventMutation.mutate(event.id) } className='scale-75' /> e.stopPropagation()} >
配置 删除
))}
)} {/* ==================== 对话框:新增事件 ==================== */} 新增通知事件 选择系统内置的事件并绑定推送渠道,创建后即可开始生效
{newEventType === 'builtin' ? (
{builtInEventsQuery.isLoading ? (
加载系统事件中...
) : availableBuiltInEvents.length === 0 ? (
所有内置事件都已配置,没有可新增的事件。
) : ( )}
) : (
{taskTypesQuery.isLoading ? (
加载异步任务中...
) : (taskTypesQuery.data ?? []).length === 0 ? (
暂无可用的系统任务。
) : ( )}
)} {newEventType === 'builtin' && newEventKey && (
事件说明: {availableBuiltInEvents.find((e) => e.key === newEventKey) ?.description || '无描述信息'}
)} {newEventType === 'task' && newEventTaskType && (
任务说明: {(taskTypesQuery.data ?? []).find( (t) => t.asynq_task === newEventTaskType, )?.description || '无描述信息'}
)}
{availableChannels.map((ch) => ( ))} {availableChannels.length === 0 && (
暂无可用渠道,请先在“通道管理与设置”中创建或启用。
)}
setNewEventTargets(e.target.value)} className='text-xs h-9' />
{newEventType === 'task' ? '支持变量:{{task_name}}, {{task_status}}, {{task_duration}}, {{user.username}}' : '支持变量:{{user.username}}, {{ip}}, {{time}}'}