import { FormEvent, useEffect, useState } from 'react' import { Bell, Loader2, Pencil, Plus, Send, Trash2 } from 'lucide-react' import toast from 'react-hot-toast' import { notifyChannelsAPI, type NotifyChannelInput, } from '../api/notify_channels' import type { NotifyChannel } from '../types' // NotifyChannelsPage replaces the Vue NotifyTab. Operators can register // multiple Telegram bots / Bark devices / WeChat keys / Webhooks and // fire a test notification on demand. export function NotifyChannelsPage() { const [channels, setChannels] = useState([]) const [loading, setLoading] = useState(true) const [editing, setEditing] = useState(null) const [showForm, setShowForm] = useState(false) const refresh = async () => { setLoading(true) try { setChannels(await notifyChannelsAPI.list()) } finally { setLoading(false) } } useEffect(() => { refresh().catch(() => undefined) }, []) const onTest = async (id: string) => { try { await notifyChannelsAPI.test(id) toast.success('测试消息已发送') } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '发送失败' toast.error(msg) } } const onDelete = async (ch: NotifyChannel) => { if (!confirm(`确定删除「${ch.name}」?`)) return try { await notifyChannelsAPI.remove(ch.id) toast.success('已删除') await refresh() } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '删除失败' toast.error(msg) } } return (

通知渠道

配置 Telegram / Bark / 企业微信 / Webhook 多通道推送

{loading && (
)} {!loading && channels.length === 0 && (
暂无通知渠道
)} {!loading && channels.length > 0 && (
{channels.map((ch) => ( onTest(ch.id)} onEdit={() => { setEditing(ch) setShowForm(true) }} onDelete={() => onDelete(ch)} /> ))}
)} {showForm && ( setShowForm(false)} onSaved={async () => { setShowForm(false) await refresh() }} /> )}
) } const TYPE_LABELS: Record = { telegram: 'Telegram', wechat: '企业微信', bark: 'Bark', webhook: 'Webhook', email: 'Email', } function ChannelCard({ channel, onTest, onEdit, onDelete, }: { channel: NotifyChannel onTest: () => void onEdit: () => void onDelete: () => void }) { const summary = channelSummary(channel) return (
{channel.name} {TYPE_LABELS[channel.channel_type] ?? channel.channel_type} {!channel.enabled && ( 已禁用 )}
{summary}
) } function channelSummary(ch: NotifyChannel): string { const cfg = ch.config ?? {} switch (ch.channel_type) { case 'telegram': return `Bot ${String(cfg.bot_token ?? '').slice(0, 10)}… → chat ${cfg.chat_id ?? '-'}` case 'wechat': return `SendKey ${String(cfg.sendkey ?? '').slice(0, 10)}…` case 'bark': return `Device ${String(cfg.device_key ?? '').slice(0, 10)}…` case 'webhook': return `${cfg.method ?? 'POST'} ${cfg.url ?? ''}` case 'email': return `SMTP ${cfg.smtp_host ?? '-'}:${cfg.smtp_port ?? '-'} → ${cfg.to ?? '-'}` default: return '' } } // ─── Form Modal ───────────────────────────────────────────────────────────── const EMPTY_CONFIG: Record> = { telegram: { bot_token: '', chat_id: '' }, wechat: { sendkey: '' }, bark: { device_key: '', server: '' }, webhook: { url: '', method: 'POST', headers: '', body_template: '' }, email: { smtp_host: '', smtp_port: '465', username: '', password: '', from: '', to: '', tls: 'true' }, } function ChannelFormModal({ editing, onClose, onSaved, }: { editing: NotifyChannel | null onClose: () => void onSaved: () => void | Promise }) { const [name, setName] = useState(editing?.name ?? '') const [type, setType] = useState( editing?.channel_type ?? 'telegram', ) const [config, setConfig] = useState>( editing?.config ?? EMPTY_CONFIG.telegram, ) const [enabled, setEnabled] = useState(editing?.enabled ?? true) const [saving, setSaving] = useState(false) const onTypeChange = (t: NotifyChannel['channel_type']) => { setType(t) setConfig({ ...EMPTY_CONFIG[t] }) } const onSubmit = async (e: FormEvent) => { e.preventDefault() setSaving(true) try { const input: NotifyChannelInput = { name: name.trim(), channel_type: type, config, enabled, } if (editing) { await notifyChannelsAPI.update(editing.id, input) } else { await notifyChannelsAPI.create(input) } toast.success('已保存') await onSaved() } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '保存失败' toast.error(msg) } finally { setSaving(false) } } const updateConfig = (k: string, v: string) => setConfig((c) => ({ ...c, [k]: v })) return (

{editing ? '编辑通知渠道' : '添加通知渠道'}

setName(e.target.value)} /> {type === 'telegram' && ( <> updateConfig('bot_token', e.target.value)} /> updateConfig('chat_id', e.target.value)} /> )} {type === 'wechat' && ( updateConfig('sendkey', e.target.value)} /> )} {type === 'bark' && ( <> updateConfig('device_key', e.target.value)} /> updateConfig('server', e.target.value)} /> )} {type === 'webhook' && ( <> updateConfig('url', e.target.value)} />