diff --git a/web/src/api/notification-channel.ts b/web/src/api/notification-channel.ts new file mode 100644 index 0000000..8e6fd60 --- /dev/null +++ b/web/src/api/notification-channel.ts @@ -0,0 +1,26 @@ +import { get, post, put } from './request'; +import type { NotificationChannel } from '../types'; + +const PROPERTY_ID_NOTIFICATION_CHANNELS = 'notification_channels'; + +// 获取通知渠道列表 +export const getNotificationChannels = async (): Promise => { + const response = await get<{ id: string; name: string; value: NotificationChannel[] }>( + `/admin/properties/${PROPERTY_ID_NOTIFICATION_CHANNELS}` + ); + return response.data.value || []; +}; + +// 保存通知渠道列表 +export const saveNotificationChannels = async (channels: NotificationChannel[]): Promise => { + await put(`/admin/properties/${PROPERTY_ID_NOTIFICATION_CHANNELS}`, { + name: '通知渠道配置', + value: channels, + }); +}; + +// 测试通知渠道(从数据库读取配置) +export const testNotificationChannel = async (type: string): Promise<{ message: string }> => { + const response = await post<{ message: string }>(`/admin/notification-channels/${type}/test`); + return response.data; +}; diff --git a/web/src/pages/Settings/NotificationChannels.tsx b/web/src/pages/Settings/NotificationChannels.tsx new file mode 100644 index 0000000..a8f6102 --- /dev/null +++ b/web/src/pages/Settings/NotificationChannels.tsx @@ -0,0 +1,344 @@ +import { useState } from 'react'; +import { Button, Table, Space, App, Modal, Form, Input, Switch, Tag, Select } from 'antd'; +import { Plus, Pencil, Trash2, TestTube } from 'lucide-react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import type { NotificationChannel } from '../../types'; +import { + getNotificationChannels, + saveNotificationChannels, + testNotificationChannel, +} from '../../api/notification-channel'; +import { getErrorMessage } from '../../lib/utils'; + +// 支持的渠道类型 +const CHANNEL_TYPES = [ + { label: '钉钉', value: 'dingtalk' }, + { label: '企业微信', value: 'wecom' }, + { label: '飞书', value: 'feishu' }, + { label: '自定义Webhook', value: 'webhook' }, +]; + +const NotificationChannels = () => { + const [form] = Form.useForm(); + const { message: messageApi, modal: modalApi } = App.useApp(); + const queryClient = useQueryClient(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isSelectModalOpen, setIsSelectModalOpen] = useState(false); + const [editingType, setEditingType] = useState(null); + const [selectedType, setSelectedType] = useState(''); + + // 获取通知渠道列表 + const { data: channels = [], isLoading } = useQuery({ + queryKey: ['notificationChannels'], + queryFn: getNotificationChannels, + }); + + // 保存 mutation + const saveMutation = useMutation({ + mutationFn: saveNotificationChannels, + onSuccess: () => { + messageApi.success('保存成功'); + queryClient.invalidateQueries({ queryKey: ['notificationChannels'] }); + setIsModalOpen(false); + setEditingType(null); + form.resetFields(); + }, + onError: (error: unknown) => { + messageApi.error(getErrorMessage(error, '保存失败')); + }, + }); + + // 测试 mutation + const testMutation = useMutation({ + mutationFn: testNotificationChannel, + onSuccess: () => { + messageApi.success('测试通知已发送'); + }, + onError: (error: unknown) => { + messageApi.error(getErrorMessage(error, '测试失败')); + }, + }); + + const handleAdd = () => { + setIsSelectModalOpen(true); + }; + + const handleSelectType = () => { + if (!selectedType) { + messageApi.warning('请选择渠道类型'); + return; + } + + // 检查是否已存在 + const exists = channels.find((ch) => ch.type === selectedType); + if (exists) { + messageApi.warning('该渠道类型已存在,请直接编辑'); + setIsSelectModalOpen(false); + return; + } + + setEditingType(selectedType); + form.setFieldsValue({ + type: selectedType, + enabled: true, + }); + setIsSelectModalOpen(false); + setIsModalOpen(true); + setSelectedType(''); + }; + + const handleEdit = (record: NotificationChannel) => { + setEditingType(record.type); + // 将配置展开到表单字段 + form.setFieldsValue({ + type: record.type, + enabled: record.enabled, + ...record.config, // 展开配置对象 + }); + setIsModalOpen(true); + }; + + const handleDelete = (type: string) => { + modalApi.confirm({ + title: '确认删除', + content: '确定要删除这个通知渠道配置吗?', + onOk: () => { + // 从列表中删除该类型的渠道 + const newChannels = channels.filter((ch) => ch.type !== type); + saveMutation.mutate(newChannels); + }, + }); + }; + + const handleTest = (type: string) => { + testMutation.mutate(type); + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const { type, enabled, ...configFields } = values; + + // 构建配置对象 + const config: Record = {}; + Object.keys(configFields).forEach((key) => { + if (configFields[key] !== undefined && configFields[key] !== '') { + config[key] = configFields[key]; + } + }); + + const newChannel: NotificationChannel = { + type, + enabled, + config, + }; + + // 更新或添加渠道 + const existingIndex = channels.findIndex((ch) => ch.type === type); + let newChannels: NotificationChannel[]; + if (existingIndex >= 0) { + // 更新现有渠道 + newChannels = [...channels]; + newChannels[existingIndex] = newChannel; + } else { + // 添加新渠道 + newChannels = [...channels, newChannel]; + } + + saveMutation.mutate(newChannels); + } catch (error) { + // 表单验证失败 + } + }; + + const getTypeLabel = (type: string) => { + const types = { + dingtalk: '钉钉', + wecom: '企业微信', + feishu: '飞书', + email: '邮件', + webhook: '自定义Webhook', + }; + return types[type as keyof typeof types] || type; + }; + + const columns = [ + { + title: '渠道类型', + dataIndex: 'type', + key: 'type', + render: (type: string) => getTypeLabel(type), + }, + { + title: '状态', + dataIndex: 'enabled', + key: 'enabled', + render: (enabled: boolean) => + enabled ? 启用 : 禁用, + }, + { + title: '操作', + key: 'action', + render: (_: unknown, record: NotificationChannel) => ( + + + + + + ), + }, + ]; + + // 获取未配置的渠道类型 + const availableTypes = CHANNEL_TYPES.filter((type) => !channels.find((ch) => ch.type === type.value)); + + return ( +
+
+
+

通知渠道管理

+

配置钉钉、企业微信、飞书和自定义Webhook通知渠道

+
+ +
+ + + + {/* 选择渠道类型对话框 */} + { + setIsSelectModalOpen(false); + setSelectedType(''); + }} + width={400} + > +
+ + + + + + + + + prev.type !== curr.type}> + {({ getFieldValue }) => { + const type = getFieldValue('type'); + if (type === 'dingtalk') { + return ( + <> + + + + + + + + ); + } + if (type === 'wecom') { + return ( + + + + ); + } + if (type === 'feishu') { + return ( + <> + + + + + + + + ); + } + if (type === 'webhook') { + return ( + + + + ); + } + return null; + }} + + +
+ + ); +}; + +export default NotificationChannels; diff --git a/web/src/pages/Settings/NotificationChannels.tsx.bak b/web/src/pages/Settings/NotificationChannels.tsx.bak new file mode 100644 index 0000000..65b2ccb --- /dev/null +++ b/web/src/pages/Settings/NotificationChannels.tsx.bak @@ -0,0 +1,296 @@ +import { useState } from 'react'; +import { Button, Table, Space, App, Modal, Form, Input, Switch, Select, Tag } from 'antd'; +import { Plus, Pencil, Trash2, TestTube } from 'lucide-react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import type { NotificationChannel } from '../../types'; +import { + getNotificationChannels, + createNotificationChannel, + updateNotificationChannel, + deleteNotificationChannel, + testNotificationChannel, +} from '../../api/notification-channel'; +import { getErrorMessage } from '../../lib/utils'; + +const NotificationChannels = () => { + const [form] = Form.useForm(); + const { message: messageApi, modal: modalApi } = App.useApp(); + const queryClient = useQueryClient(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingChannel, setEditingChannel] = useState(null); + + // 获取通知渠道列表 + const { data: channels = [], isLoading } = useQuery({ + queryKey: ['notificationChannels'], + queryFn: getNotificationChannels, + }); + + // 创建 mutation + const createMutation = useMutation({ + mutationFn: createNotificationChannel, + onSuccess: () => { + messageApi.success('创建成功'); + queryClient.invalidateQueries({ queryKey: ['notificationChannels'] }); + setIsModalOpen(false); + form.resetFields(); + }, + onError: (error: unknown) => { + messageApi.error(getErrorMessage(error, '创建失败')); + }, + }); + + // 更新 mutation + const updateMutation = useMutation({ + mutationFn: ({ id, channel }: { id: string; channel: NotificationChannel }) => + updateNotificationChannel(id, channel), + onSuccess: () => { + messageApi.success('更新成功'); + queryClient.invalidateQueries({ queryKey: ['notificationChannels'] }); + setIsModalOpen(false); + setEditingChannel(null); + form.resetFields(); + }, + onError: (error: unknown) => { + messageApi.error(getErrorMessage(error, '更新失败')); + }, + }); + + // 删除 mutation + const deleteMutation = useMutation({ + mutationFn: deleteNotificationChannel, + onSuccess: () => { + messageApi.success('删除成功'); + queryClient.invalidateQueries({ queryKey: ['notificationChannels'] }); + }, + onError: (error: unknown) => { + messageApi.error(getErrorMessage(error, '删除失败')); + }, + }); + + // 测试 mutation + const testMutation = useMutation({ + mutationFn: testNotificationChannel, + onSuccess: () => { + messageApi.success('测试通知已发送'); + }, + onError: (error: unknown) => { + messageApi.error(getErrorMessage(error, '测试失败')); + }, + }); + + const handleCreate = () => { + setEditingChannel(null); + form.resetFields(); + setIsModalOpen(true); + }; + + const handleEdit = (record: NotificationChannel) => { + setEditingChannel(record); + form.setFieldsValue(record); + setIsModalOpen(true); + }; + + const handleDelete = (id: string) => { + modalApi.confirm({ + title: '确认删除', + content: '确定要删除这个通知渠道吗?', + onOk: () => deleteMutation.mutate(id), + }); + }; + + const handleTest = (id: string) => { + testMutation.mutate(id); + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + if (editingChannel?.id) { + updateMutation.mutate({ id: editingChannel.id, channel: values }); + } else { + createMutation.mutate(values); + } + } catch (error) { + // 表单验证失败 + } + }; + + const getTypeLabel = (type: string) => { + const types = { + dingtalk: '钉钉', + wecom: '企业微信', + feishu: '飞书', + email: '邮件', + webhook: '自定义Webhook', + }; + return types[type as keyof typeof types] || type; + }; + + const columns = [ + { + title: '名称', + dataIndex: 'name', + key: 'name', + }, + { + title: '类型', + dataIndex: 'type', + key: 'type', + render: (type: string) => getTypeLabel(type), + }, + { + title: '状态', + dataIndex: 'enabled', + key: 'enabled', + render: (enabled: boolean) => + enabled ? 启用 : 禁用, + }, + { + title: '操作', + key: 'action', + render: (_: unknown, record: NotificationChannel) => ( + + + + + + ), + }, + ]; + + return ( +
+
+

通知渠道管理

+ +
+ +
+ + { + setIsModalOpen(false); + setEditingChannel(null); + form.resetFields(); + }} + confirmLoading={createMutation.isPending || updateMutation.isPending} + width={600} + > +
+ + + + + + + + + + + + ); + } + if (type === 'wecom') { + return ( + + + + ); + } + if (type === 'feishu') { + return ( + + + + ); + } + if (type === 'webhook') { + return ( + + + + ); + } + return null; + }} + + +
+ + ); +}; + +export default NotificationChannels;