// 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 {AdminService} from "@/lib/services/admin" import {PushService} from "@/lib/services/push" import type {CreatePushEventRequest, PushEvent, UpdatePushEventRequest,} 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: () => AdminService.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 data-[state=checked]:bg-green-600 h-4 w-7" /> 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}}"}